Skip to content

Commit 91ea669

Browse files
authored
Merge pull request #10 from Eliav2:dev
0.3.0
2 parents f9e4c19 + 1f387d0 commit 91ea669

14 files changed

+361
-172
lines changed

Diff for: README.md

+155-89
Original file line numberDiff line numberDiff line change
@@ -50,34 +50,34 @@ use `TypedRouter` from `express-typed` instead of `express.Router`, the rest of
5050

5151
```typescript
5252
import express from "express";
53-
import {TypedRouter, ParseRoutes} from "express-typed";
53+
import { TypedRouter, ParseRoutes } from "express-typed";
5454

5555
const app = express();
5656

5757
//// THIS:
5858
const router = express.Router();
5959

6060
router.get("/", async (req: Request, res: Response) => {
61-
res.send("Hello World!").status(200);
61+
res.send("Hello World!").status(200);
6262
});
6363
router.post("/", async (req, res) => {
64-
res.send(req.body).status(200);
64+
res.send(req.body).status(200);
6565
});
6666

6767
app.use(router);
6868
//// -->
6969
//// BECOMES THIS:
7070
const typedRouter = new TypedRouter({
71-
get: {
72-
"/": async (req, res) => {
73-
res.send("Hello World!").status(200);
74-
},
71+
get: {
72+
"/": async (req, res) => {
73+
res.send("Hello World!").status(200);
7574
},
76-
post: {
77-
"/": async (req, res) => {
78-
res.send(req.body).status(200);
79-
},
75+
},
76+
post: {
77+
"/": async (req, res) => {
78+
res.send(req.body).status(200);
8079
},
80+
},
8181
});
8282

8383
app.use(typedRouter.router);
@@ -87,7 +87,7 @@ export type AppRoutes = ParseRoutes<typeof typedRouter>;
8787
/////
8888

8989
app.listen(3000, () => {
90-
console.log("Server is running on port 3000");
90+
console.log("Server is running on port 3000");
9191
});
9292
```
9393

@@ -150,12 +150,12 @@ utilizing helper types from `express-typed`.
150150
`RouteResResolver` is used to extract the response type from a specific route.
151151

152152
```ts
153-
import {GetRouteResponseInfo, GetRouteResponseInfoHelper} from "express-typed";
153+
import { GetRouteResponseInfo, GetRouteResponseInfoHelper } from "express-typed";
154154
//// RouteResResolver
155155
export type RouteResResolver<
156-
Path extends keyof AppRoutes,
157-
Method extends keyof AppRoutes[Path],
158-
Info extends keyof GetRouteResponseInfoHelper<AppRoutes, Path, Method> | "body" = "body"
156+
Path extends keyof AppRoutes,
157+
Method extends keyof AppRoutes[Path],
158+
Info extends keyof GetRouteResponseInfoHelper<AppRoutes, Path, Method> | "body" = "body"
159159
> = GetRouteResponseInfo<AppRoutes, Path, Method, Info>;
160160
```
161161

@@ -183,38 +183,38 @@ and `react-query`(from [here](/examples/fullstack_react_express-typed/frontend-d
183183

184184
```ts
185185
// queries.ts
186-
import {useQuery} from "@tanstack/react-query";
187-
import axios, {type AxiosStatic} from "axios";
188-
import type {AppRoutes, RouteResResolver} from "your-backend-package/src/routes/index.routes";
186+
import { useQuery } from "@tanstack/react-query";
187+
import axios, { type AxiosStatic } from "axios";
188+
import type { AppRoutes, RouteResResolver } from "your-backend-package/src/routes/index.routes";
189189

190190
// an hook to fetch response from server, for any possible method(GET, POST, PUT, DELETE)
191191
export const useAppQuery = <Path extends keyof AppRoutes, Method extends Extract<keyof AxiosStatic, keyof AppRoutes[Path]>>(
192-
path: Path,
193-
method: Method
192+
path: Path,
193+
method: Method
194194
) => {
195-
return useQuery<RouteResResolver<Path, Method>>({
196-
queryKey: [path],
197-
queryFn: async () => {
198-
const res = await (axios as any)[method](`/api${path}`);
199-
return res.data as RouteResResolver<Path, Method>;
200-
},
201-
});
195+
return useQuery<RouteResResolver<Path, Method>>({
196+
queryKey: [path],
197+
queryFn: async () => {
198+
const res = await (axios as any)[method](`/api${path}`);
199+
return res.data as RouteResResolver<Path, Method>;
200+
},
201+
});
202202
};
203203
```
204204

205205
and usage(see [here](/examples/fullstack_react_express-typed/frontend-demo/src/App.tsx)):
206206

207207
```tsx
208-
import {useAppQuery} from "./queries";
208+
import { useAppQuery } from "./queries";
209209

210210
function App() {
211-
const query = useAppQuery("/", "get");
212-
const data = query.data;
213-
// ^? const query: UseQueryResult<"Hello world", Error>
211+
const query = useAppQuery("/", "get");
212+
const data = query.data;
213+
// ^? const query: UseQueryResult<"Hello world", Error>
214214

215-
console.log("data", data);
215+
console.log("data", data);
216216

217-
return <>{JSON.stringify(data)}</>;
217+
return <>{JSON.stringify(data)}</>;
218218
}
219219

220220
export default App;
@@ -233,22 +233,22 @@ export default App;
233233
`RouteReqResolver` is defined on your side with the help of `GetRouteRequestHelper` and `GetRouteRequest`:
234234

235235
```ts
236-
import {GetRouteRequestHelper, GetRouteRequest, TypedRouter} from "express-typed";
236+
import { GetRouteRequestHelper, GetRouteRequest, TypedRouter } from "express-typed";
237237

238238
export type RouteReqResolver<
239-
Path extends keyof AppRoutes,
240-
Method extends keyof AppRoutes[Path],
241-
Info extends keyof GetRouteRequestHelper<AppRoutes, Path, Method> = Extract<keyof GetRouteRequestHelper<AppRoutes, Path, Method>, "body">
239+
Path extends keyof AppRoutes,
240+
Method extends keyof AppRoutes[Path],
241+
Info extends keyof GetRouteRequestHelper<AppRoutes, Path, Method> = Extract<keyof GetRouteRequestHelper<AppRoutes, Path, Method>, "body">
242242
> = GetRouteRequest<AppRoutes, Path, Method, Info>;
243243

244244
const typedRouter = new TypedRouter({
245-
"/": {
246-
get: (req: TypedRequest<{ body: "bb"; query: "qq" }>, res) => {
247-
const body = req.body;
248-
const test = res.send("Home").status(200);
249-
return test;
250-
},
245+
"/": {
246+
get: (req: TypedRequest<{ body: "bb"; query: "qq" }>, res) => {
247+
const body = req.body;
248+
const test = res.send("Home").status(200);
249+
return test;
251250
},
251+
},
252252
});
253253

254254
type HomePageBody = RouteReqResolver<"/", "get">;
@@ -261,24 +261,24 @@ example of using this info with react-query mutation(
261261
see [here](/examples/fullstack_react_express-typed/frontend-demo/src/mutations.ts)):
262262

263263
```ts
264-
import {DefaultError, useMutation} from "@tanstack/react-query";
264+
import { DefaultError, useMutation } from "@tanstack/react-query";
265265
import axios from "axios";
266-
import {AppRoutes, RouteReqResolver, RouteResResolver} from "express-typed-demo/src/routes/index.routes";
266+
import { AppRoutes, RouteReqResolver, RouteResResolver } from "express-typed-demo/src/routes/index.routes";
267267

268268
const useAppMutation = <Path extends keyof AppRoutes, Method extends keyof AppRoutes[Path]>(path: Path, method: Method) => {
269-
const mutation = useMutation<RouteResResolver<Path, Method>, DefaultError, RouteReqResolver<Path, Method>>({
270-
mutationKey: ["mutation", path, method],
271-
mutationFn: async () => {
272-
const res = await (axios as any)[method](`/api${path}`);
273-
return res.data as RouteResResolver<Path, Method>;
274-
},
275-
});
276-
return mutation;
269+
const mutation = useMutation<RouteResResolver<Path, Method>, DefaultError, RouteReqResolver<Path, Method>>({
270+
mutationKey: ["mutation", path, method],
271+
mutationFn: async () => {
272+
const res = await (axios as any)[method](`/api${path}`);
273+
return res.data as RouteResResolver<Path, Method>;
274+
},
275+
});
276+
return mutation;
277277
};
278278

279279
// completly type safe
280280
const testMutation = useAppMutation("/mutate", "post");
281-
testMutation.mutate({name: "test"});
281+
testMutation.mutate({ name: "test" });
282282
```
283283

284284
</details>
@@ -294,7 +294,7 @@ testMutation.mutate({name: "test"});
294294
`RoutesWithMethod` is used to extract all the routes with a specific method from the routes object.
295295

296296
```ts
297-
import {GetRoutesWithMethod, GetRouterMethods} from "express-typed";
297+
import { GetRoutesWithMethod, GetRouterMethods } from "express-typed";
298298
//// RoutesWithMethod
299299
export type RoutesWithMethod<Method extends GetRouterMethods<AppRoutes>> = GetRoutesWithMethod<AppRoutes, Method>;
300300
```
@@ -314,32 +314,32 @@ type PostRoutes = RoutesWithMethod<"post">;
314314
then in your frontend codebase, you can define the following react-query hooks:
315315

316316
```ts
317-
import {useQuery} from "@tanstack/react-query";
317+
import { useQuery } from "@tanstack/react-query";
318318
import axios from "axios";
319-
import type {RoutesWithMethod} from "express-typed-demo/src/routes/index.routes";
319+
import type { RoutesWithMethod } from "express-typed-demo/src/routes/index.routes";
320320

321321
// an hook to fetch response from server, for GET method
322322
type GetRoutes = RoutesWithMethod<"get">;
323323
export const useAppGetQuery = <P extends keyof GetRoutes>(path: P) => {
324-
return useQuery<GetRoutes[P]>({
325-
queryKey: [path],
326-
queryFn: async () => {
327-
const res = await axios.get(`/api${path}`);
328-
return res.data as GetRoutes[P];
329-
},
330-
});
324+
return useQuery<GetRoutes[P]>({
325+
queryKey: [path],
326+
queryFn: async () => {
327+
const res = await axios.get(`/api${path}`);
328+
return res.data as GetRoutes[P];
329+
},
330+
});
331331
};
332332

333333
// an hook to fetch response from server, for POST method
334334
type PostRoutes = RoutesWithMethod<"post">;
335335
export const useAppPostQuery = <P extends keyof PostRoutes>(path: P) => {
336-
return useQuery<PostRoutes[P]>({
337-
queryKey: [path],
338-
queryFn: async () => {
339-
const res = await axios.post(`/api${path}`);
340-
return res.data as PostRoutes[P];
341-
},
342-
});
336+
return useQuery<PostRoutes[P]>({
337+
queryKey: [path],
338+
queryFn: async () => {
339+
const res = await axios.post(`/api${path}`);
340+
return res.data as PostRoutes[P];
341+
},
342+
});
343343
};
344344
```
345345

@@ -357,19 +357,19 @@ see full example [here](/examples/fullstack_react_express-typed/express-typed-de
357357

358358
```typescript
359359
import {
360-
GetRouteRequest,
361-
GetRouteRequestHelper,
362-
GetRouteResponseInfo,
363-
GetRouteResponseInfoHelper,
364-
GetRouterMethods,
365-
GetRoutesWithMethod,
366-
ParseRoutes,
367-
TypedRequest,
368-
TypedRouter,
360+
GetRouteRequest,
361+
GetRouteRequestHelper,
362+
GetRouteResponseInfo,
363+
GetRouteResponseInfoHelper,
364+
GetRouterMethods,
365+
GetRoutesWithMethod,
366+
ParseRoutes,
367+
TypedRequest,
368+
TypedRouter,
369369
} from "express-typed";
370370

371371
const typedRouter = new TypedRouter({
372-
// your routes here
372+
// your routes here
373373
});
374374

375375
export default typedRouter;
@@ -381,23 +381,88 @@ export default typedRouter;
381381
export type AppRoutes = ParseRoutes<typeof typedRouter>;
382382

383383
export type RouteResResolver<
384-
// example usage
385-
Path extends keyof AppRoutes,
386-
Method extends keyof AppRoutes[Path],
387-
Info extends keyof GetRouteResponseInfoHelper<AppRoutes, Path, Method> | "body" = "body"
384+
// example usage
385+
Path extends keyof AppRoutes,
386+
Method extends keyof AppRoutes[Path],
387+
Info extends keyof GetRouteResponseInfoHelper<AppRoutes, Path, Method> | "body" = "body"
388388
> = GetRouteResponseInfo<AppRoutes, Path, Method, Info>;
389389

390390
export type RouteReqResolver<
391-
Path extends keyof AppRoutes,
392-
Method extends keyof AppRoutes[Path],
393-
Info extends keyof GetRouteRequestHelper<AppRoutes, Path, Method> = Extract<keyof GetRouteRequestHelper<AppRoutes, Path, Method>, "body">
391+
Path extends keyof AppRoutes,
392+
Method extends keyof AppRoutes[Path],
393+
Info extends keyof GetRouteRequestHelper<AppRoutes, Path, Method> = Extract<keyof GetRouteRequestHelper<AppRoutes, Path, Method>, "body">
394394
> = GetRouteRequest<AppRoutes, Path, Method, Info>;
395395

396396
export type RoutesWithMethod<Method extends GetRouterMethods<AppRoutes>> = GetRoutesWithMethod<AppRoutes, Method>;
397397
```
398398

399399
</details>
400400

401+
## Quick walkthrough
402+
403+
This demo highlights the key usage and features of express-typed, including type inference, explicit typing, and nested routes. follow the comments line by line to understand the usage better.
404+
405+
```ts
406+
import { TypedRequest, TypedResponse, TypedRouter, ParseRoutes } from "express-typed";
407+
408+
const typedRouter = new TypedRouter({
409+
// returned type is inferred
410+
"/": {
411+
get: (req, res) => {
412+
return res.send("get: /").status(200);
413+
},
414+
post: (req, res) => {
415+
return res.send("post: /").status(200);
416+
},
417+
},
418+
// request body is explicitly typed, response is inferred based on the return value
419+
"/explicit-req": {
420+
get: (req: TypedRequest<{ body: { name: string } }>, res) => {
421+
const body = req.body;
422+
// ^?
423+
return res.json(req.body).status(200);
424+
},
425+
},
426+
// response body is explicitly typed, retrun type must at least extend { name: string }
427+
"/explicit-res": {
428+
get: (req, res: TypedResponse<{ body: { name: string } }>) => {
429+
return res.json({ name: "eliav" }).status(200);
430+
},
431+
},
432+
// nested router are allowed, and fully typed
433+
"/nested": new TypedRouter({
434+
"/": {
435+
get: (req, res) => {
436+
const test = res.send("get /nested/").status(200);
437+
return test;
438+
},
439+
// async methods are supported
440+
post: async (req, res) => {
441+
const test = (await (await fetch("https://jsonplaceholder.typicode.com/todos/1")).json()) as {
442+
userId: number;
443+
id: number;
444+
title: string;
445+
completed: boolean;
446+
};
447+
return res.json(test).status(200);
448+
},
449+
},
450+
// any of "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head" is allowed as a method
451+
"/all": {
452+
all: (req, res) => {
453+
return res.send("responding to all methods");
454+
},
455+
},
456+
}),
457+
});
458+
459+
export default typedRouter;
460+
461+
export type AppRoutes = ParseRoutes<typeof typedRouter>;
462+
```
463+
464+
and pretty much, that's it! you can now use the types defined in `AppRoutes` to ensure type safety in your frontend codebase.
465+
401466
## Contributing
402467

403468
This library is still in its early stages, and that's exactly the time to suggest significant changes.
@@ -416,5 +481,6 @@ like Fastify, Koa, etc.
416481
- [x] nested routers support
417482
- [x] backend return type inference(the type that the backend returns)
418483
- [x] backend request type inference(the type that the backend expects in the request)
484+
- [x] explicitly typed request/response
419485
- [ ] type-safe path parameters
420486
- [ ] type-safe query parameters

0 commit comments

Comments
 (0)