@@ -50,34 +50,34 @@ use `TypedRouter` from `express-typed` instead of `express.Router`, the rest of
50
50
51
51
``` typescript
52
52
import express from " express" ;
53
- import {TypedRouter , ParseRoutes } from " express-typed" ;
53
+ import { TypedRouter , ParseRoutes } from " express-typed" ;
54
54
55
55
const app = express ();
56
56
57
57
// // THIS:
58
58
const router = express .Router ();
59
59
60
60
router .get (" /" , async (req : Request , res : Response ) => {
61
- res .send (" Hello World!" ).status (200 );
61
+ res .send (" Hello World!" ).status (200 );
62
62
});
63
63
router .post (" /" , async (req , res ) => {
64
- res .send (req .body ).status (200 );
64
+ res .send (req .body ).status (200 );
65
65
});
66
66
67
67
app .use (router );
68
68
// // -->
69
69
// // BECOMES THIS:
70
70
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 );
75
74
},
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 );
80
79
},
80
+ },
81
81
});
82
82
83
83
app .use (typedRouter .router );
@@ -87,7 +87,7 @@ export type AppRoutes = ParseRoutes<typeof typedRouter>;
87
87
// ///
88
88
89
89
app .listen (3000 , () => {
90
- console .log (" Server is running on port 3000" );
90
+ console .log (" Server is running on port 3000" );
91
91
});
92
92
```
93
93
@@ -150,12 +150,12 @@ utilizing helper types from `express-typed`.
150
150
` RouteResResolver ` is used to extract the response type from a specific route.
151
151
152
152
``` ts
153
- import {GetRouteResponseInfo , GetRouteResponseInfoHelper } from " express-typed" ;
153
+ import { GetRouteResponseInfo , GetRouteResponseInfoHelper } from " express-typed" ;
154
154
// // RouteResResolver
155
155
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"
159
159
> = GetRouteResponseInfo <AppRoutes , Path , Method , Info >;
160
160
```
161
161
@@ -183,38 +183,38 @@ and `react-query`(from [here](/examples/fullstack_react_express-typed/frontend-d
183
183
184
184
``` ts
185
185
// 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" ;
189
189
190
190
// an hook to fetch response from server, for any possible method(GET, POST, PUT, DELETE)
191
191
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
194
194
) => {
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
+ });
202
202
};
203
203
```
204
204
205
205
and usage(see [ here] ( /examples/fullstack_react_express-typed/frontend-demo/src/App.tsx ) ):
206
206
207
207
``` tsx
208
- import {useAppQuery } from " ./queries" ;
208
+ import { useAppQuery } from " ./queries" ;
209
209
210
210
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>
214
214
215
- console .log (" data" , data );
215
+ console .log (" data" , data );
216
216
217
- return <>{ JSON .stringify (data )} </>;
217
+ return <>{ JSON .stringify (data )} </>;
218
218
}
219
219
220
220
export default App ;
@@ -233,22 +233,22 @@ export default App;
233
233
` RouteReqResolver ` is defined on your side with the help of ` GetRouteRequestHelper ` and ` GetRouteRequest ` :
234
234
235
235
``` ts
236
- import {GetRouteRequestHelper , GetRouteRequest , TypedRouter } from " express-typed" ;
236
+ import { GetRouteRequestHelper , GetRouteRequest , TypedRouter } from " express-typed" ;
237
237
238
238
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" >
242
242
> = GetRouteRequest <AppRoutes , Path , Method , Info >;
243
243
244
244
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 ;
251
250
},
251
+ },
252
252
});
253
253
254
254
type HomePageBody = RouteReqResolver <" /" , " get" >;
@@ -261,24 +261,24 @@ example of using this info with react-query mutation(
261
261
see [ here] ( /examples/fullstack_react_express-typed/frontend-demo/src/mutations.ts ) ):
262
262
263
263
``` ts
264
- import {DefaultError , useMutation } from " @tanstack/react-query" ;
264
+ import { DefaultError , useMutation } from " @tanstack/react-query" ;
265
265
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" ;
267
267
268
268
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 ;
277
277
};
278
278
279
279
// completly type safe
280
280
const testMutation = useAppMutation (" /mutate" , " post" );
281
- testMutation .mutate ({name: " test" });
281
+ testMutation .mutate ({ name: " test" });
282
282
```
283
283
284
284
</details >
@@ -294,7 +294,7 @@ testMutation.mutate({name: "test"});
294
294
` RoutesWithMethod ` is used to extract all the routes with a specific method from the routes object.
295
295
296
296
``` ts
297
- import {GetRoutesWithMethod , GetRouterMethods } from " express-typed" ;
297
+ import { GetRoutesWithMethod , GetRouterMethods } from " express-typed" ;
298
298
// // RoutesWithMethod
299
299
export type RoutesWithMethod <Method extends GetRouterMethods <AppRoutes >> = GetRoutesWithMethod <AppRoutes , Method >;
300
300
```
@@ -314,32 +314,32 @@ type PostRoutes = RoutesWithMethod<"post">;
314
314
then in your frontend codebase, you can define the following react-query hooks:
315
315
316
316
``` ts
317
- import {useQuery } from " @tanstack/react-query" ;
317
+ import { useQuery } from " @tanstack/react-query" ;
318
318
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" ;
320
320
321
321
// an hook to fetch response from server, for GET method
322
322
type GetRoutes = RoutesWithMethod <" get" >;
323
323
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
+ });
331
331
};
332
332
333
333
// an hook to fetch response from server, for POST method
334
334
type PostRoutes = RoutesWithMethod <" post" >;
335
335
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
+ });
343
343
};
344
344
```
345
345
@@ -357,19 +357,19 @@ see full example [here](/examples/fullstack_react_express-typed/express-typed-de
357
357
358
358
``` typescript
359
359
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 ,
369
369
} from " express-typed" ;
370
370
371
371
const typedRouter = new TypedRouter ({
372
- // your routes here
372
+ // your routes here
373
373
});
374
374
375
375
export default typedRouter ;
@@ -381,23 +381,88 @@ export default typedRouter;
381
381
export type AppRoutes = ParseRoutes <typeof typedRouter >;
382
382
383
383
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"
388
388
> = GetRouteResponseInfo <AppRoutes , Path , Method , Info >;
389
389
390
390
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" >
394
394
> = GetRouteRequest <AppRoutes , Path , Method , Info >;
395
395
396
396
export type RoutesWithMethod <Method extends GetRouterMethods <AppRoutes >> = GetRoutesWithMethod <AppRoutes , Method >;
397
397
```
398
398
399
399
</details >
400
400
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
+
401
466
## Contributing
402
467
403
468
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.
416
481
- [x] nested routers support
417
482
- [x] backend return type inference(the type that the backend returns)
418
483
- [x] backend request type inference(the type that the backend expects in the request)
484
+ - [x] explicitly typed request/response
419
485
- [ ] type-safe path parameters
420
486
- [ ] type-safe query parameters
0 commit comments