|
| 1 | +# @api-ts/typed-express-router |
| 2 | + |
| 3 | +A thin wrapper around Express's `Router` |
| 4 | + |
| 5 | +## Goals |
| 6 | + |
| 7 | +- Define Express routes that are associated with routes in an api-ts `apiSpec` |
| 8 | +- Augment the existing Express request with the decoded request object |
| 9 | +- Augment the existing Express response with a type-checked `encode` function |
| 10 | +- Allow customization of what to do on decode/encode errors, per-route if desired |
| 11 | +- Allow action to be performed after an encoded response is sent, per-route if desired |
| 12 | +- Allow routes to be defined with path that is different than the one specified in the |
| 13 | + `httpRoute` (e.g. for aliases) |
| 14 | +- Follow the express router api as closely as possible otherwise |
| 15 | + |
| 16 | +## Non-Goals |
| 17 | + |
| 18 | +- Enforce that all routes listed in an `apiSpec` have an associated route handler |
| 19 | +- Layer anything on top of the `express.RequestHandler[]` chain beyond the additional |
| 20 | + properties described in `Goals` (projects and other libraries can do this) |
| 21 | + |
| 22 | +## Usage |
| 23 | + |
| 24 | +### Creating a router |
| 25 | + |
| 26 | +Two very similar functions are provided by this library that respectively create or wrap |
| 27 | +an Express router: |
| 28 | + |
| 29 | +```ts |
| 30 | +import { createRouter, wrapRouter } from '@api-ts/typed-express-router'; |
| 31 | +import express from 'express'; |
| 32 | + |
| 33 | +import { MyApi } from 'my-api-package'; |
| 34 | + |
| 35 | +const app = express(); |
| 36 | + |
| 37 | +const typedRouter = createRouter(MyApi); |
| 38 | +app.use(typedRouter); |
| 39 | +``` |
| 40 | + |
| 41 | +### Adding routes |
| 42 | + |
| 43 | +Once you have the `typedRouter`, you can start adding routes by the api-ts api name: |
| 44 | + |
| 45 | +```ts |
| 46 | +typedRouter.get('hello.world', [HelloWorldHandler]); |
| 47 | +``` |
| 48 | + |
| 49 | +Here, `HelloWorldHandler` is a almost like an Express request handler, but `req` and |
| 50 | +`res` have an extra property. `req.decoded` contains the validated and decoded request. |
| 51 | +On the response side, there is an extra `res.sendEncoded(status, payload)` function that |
| 52 | +will enforce types on the payload and encode types appropriately (e.g. |
| 53 | +`BigIntFromString` will be converted to a string). The exported `TypedRequestHandler` |
| 54 | +type may be used to infer the parameter types for these functions. |
| 55 | + |
| 56 | +### Aliased routes |
| 57 | + |
| 58 | +If more flexibility is needed in the route path, the `getAlias`-style route functions |
| 59 | +may be used. They take a path that is directly interpreted by Express, but otherwise |
| 60 | +work like the regular route methods: |
| 61 | + |
| 62 | +```ts |
| 63 | +typedRouter.getAlias('/oldDeprecatedHelloWorld', 'hello.world', [HelloWorldHandler]); |
| 64 | +``` |
| 65 | + |
| 66 | +### Unchecked routes |
| 67 | + |
| 68 | +For convenience, the original router's `get`/`post`/`put`/`delete` methods can still be |
| 69 | +used via `getUnchecked` (or similar): |
| 70 | + |
| 71 | +```ts |
| 72 | +// Just a normal express route |
| 73 | +typedRouter.getUnchecked('/api/foo/bar', (req, res) => { |
| 74 | + res.send(200).end(); |
| 75 | +}); |
| 76 | +``` |
| 77 | + |
| 78 | +### Hooks and error handlers |
| 79 | + |
| 80 | +The `createRouter`, `wrapRouter`, and individual route methods all take an optional last |
| 81 | +parameter where a post-response and error handling function may be provided. Ones |
| 82 | +specified for a specific route take precedence over the top-level ones. These may be |
| 83 | +used to customize error responses and perform other actions like metrics collection or |
| 84 | +logging. |
| 85 | + |
| 86 | +```ts |
| 87 | +const typedRouter = createRouter(MyApi, { |
| 88 | + onDecodeError: (errs, req, res) => { |
| 89 | + // Format `errs` however you want |
| 90 | + res.send(400).json({ message: 'Bad request' }).end(); |
| 91 | + }, |
| 92 | + onEncodeError: (err, req, res) => { |
| 93 | + // Ideally won't happen unless type safety is violated, so it's a 500 |
| 94 | + res.send(500).json({ message: 'Internal server error' }).end(); |
| 95 | + }, |
| 96 | + afterEncodedResponseSent: (status, payload, req, res) => { |
| 97 | + // Perform side effects or other things, `res` should be ended by this point |
| 98 | + endRequestMetricsCollection(req); |
| 99 | + }, |
| 100 | +}); |
| 101 | + |
| 102 | +// Override the decode error handler on one route |
| 103 | +typedRouter.get('hello.world', [HelloWorldHandler], { |
| 104 | + onDecodeError: customHelloDecodeErrorHandler, |
| 105 | +}); |
| 106 | +``` |
| 107 | + |
| 108 | +### Other usage |
| 109 | + |
| 110 | +Other than what is documented above, a wrapped router should behave like a regular |
| 111 | +Express one, so things like `typedRouter.use()` should behave the same. |
0 commit comments