Skip to content

Commit 6bdd8b4

Browse files
authored
Merge pull request #1041 from BitGo/docs/typed-router-cleanup
docs(typed-express-router): Refactor documentation into Diátaxis Reference section
2 parents c7289f9 + ae5f7e4 commit 6bdd8b4

File tree

8 files changed

+595
-0
lines changed

8 files changed

+595
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Configuration Options
2+
3+
You can provide configuration options, primarily hooks for handling errors and
4+
post-response actions, globally when creating/wrapping a router or on a per-route basis.
5+
Per-route options override global ones.
6+
7+
## Global Options (`TypedRouterOptions`)
8+
9+
Passed as the optional second argument to [`createRouter`](./create-router) or the
10+
optional third argument to [`wrapRouter`](./wrap-router).
11+
12+
```typescript
13+
import express from 'express';
14+
import * as t from 'io-ts'; // For Errors type
15+
import { ApiSpec } from '@api-ts/io-ts-http'; // Conceptual
16+
17+
// Simplified representation of hook signatures
18+
type DecodeErrorHandler<Req = any, Res = any> = (
19+
errors: t.Errors,
20+
req: express.Request & { decoded?: any }, // May not have decoded fully
21+
res: express.Response & { sendEncoded?: any },
22+
next: express.NextFunction,
23+
) => void;
24+
25+
type EncodeErrorHandler<Req = any, Res = any> = (
26+
error: unknown, // The error during encoding/validation
27+
req: express.Request & { decoded?: any },
28+
res: express.Response & { sendEncoded?: any },
29+
next: express.NextFunction,
30+
) => void;
31+
32+
type AfterResponseHandler<Req = any, Res = any> = (
33+
status: number,
34+
payload: any, // The successfully encoded payload
35+
req: express.Request & { decoded?: any },
36+
res: express.Response & { sendEncoded?: any },
37+
) => void;
38+
39+
export type TypedRouterOptions<T extends ApiSpec = any> = {
40+
onDecodeError?: DecodeErrorHandler;
41+
onEncodeError?: EncodeErrorHandler;
42+
afterEncodedResponseSent?: AfterResponseHandler;
43+
};
44+
```
45+
46+
- `onDecodeError(errors, req, res, next)`:
47+
- **Triggered**: When using a "checked" route method (such as `.get`) and the incoming
48+
request fails decoding or validation against the `httpRoute`'s `request` codec.
49+
- **Purpose**: Allows custom formatting and sending of error responses (such as 400
50+
Bad Request). If not provided, a default basic error handler might be used or the
51+
error might propagate.
52+
- `errors`: The `t.Errors` array from `io-ts` detailing the validation failures.
53+
- **Note**: You typically end the response (`res.status(...).json(...).end()`) within
54+
this handler. Calling `next()` might lead to unexpected behavior.
55+
- `onEncodeError(error, req, res, next)`:
56+
- **Triggered**: When `res.sendEncoded(status, payload)` is called, but the provided
57+
`payload` fails validation against the `httpRoute`'s `response` codec for the given
58+
`status`.
59+
- **Purpose**: Handles server-side errors where the application tries to send data
60+
inconsistent with the API specification. This usually indicates a bug.
61+
- `error`: The validation error encountered.
62+
- **Note**: You typically send a 500 Internal Server Error response here and should
63+
end the response.
64+
- `afterEncodedResponseSent(status, payload, req, res)`:
65+
- **Triggered**: After `res.sendEncoded(status, payload)` has successfully validated,
66+
encoded, and finished sending the response.
67+
- **Purpose**: Lets you perform side-effects after a successful response, such as
68+
logging, metrics collection, cleanup, etc.
69+
- `status`: The status code that was sent.
70+
- `payload`: The original (pre-encoding) payload object that was sent.
71+
- **Note**: The response stream (`res`) is likely ended at this point. Don't attempt
72+
to send further data.
73+
74+
## Per-Route Options (`RouteOptions`)
75+
76+
Pass these as the optional third argument to the route definition methods (such as
77+
`typedRouter.get(..., ..., routeOptions)`).
78+
79+
```typescript
80+
// RouteOptions includes the global hooks plus routeAliases
81+
export type RouteOptions<RouteDef = any> = TypedRouterOptions & {
82+
routeAliases?: string[];
83+
};
84+
```
85+
86+
- `onDecodeError` / `onEncodeError` / `afterEncodedResponseSent`: Same hooks as the
87+
global options, but these versions apply only to the specific route they're defined on
88+
and take precedence over any global hooks defined via `createRouter` or `wrapRouter`.
89+
- `routeAliases` (`string[]`):
90+
- An array of additional path strings that should also map to this route handler.
91+
- Uses Express path syntax (such as `/path/:param`).
92+
- See [`TypedRouter` Object](./typed-router) for more details and caveats regarding
93+
path parameters.
94+
95+
## Example (Global and Per-Route):
96+
97+
```typescript
98+
import { createRouter } from '@api-ts/typed-express-router';
99+
import { MyApi } from 'my-api-package';
100+
101+
// Global options
102+
const typedRouter = createRouter(MyApi, {
103+
onDecodeError: globalDecodeErrorHandler,
104+
afterEncodedResponseSent: globalMetricsHandler,
105+
});
106+
107+
// Per-route options overriding global and adding alias
108+
typedRouter.get('some.operation', [myHandler], {
109+
routeAliases: ['/legacy/path'],
110+
onDecodeError: specificDecodeErrorHandler, // Overrides globalDecodeErrorHandler for this route
111+
// afterEncodedResponseSent is inherited from global options
112+
});
113+
114+
typedRouter.post('another.operation', [otherHandler], {
115+
// Inherits onDecodeError from global options
116+
// No afterEncodedResponseSent hook will run for this route
117+
afterEncodedResponseSent: undefined, // Explicitly disable global hook for this route
118+
});
119+
```
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# `createRouter`
2+
3+
Creates a new Express Router instance that's typed according to a provided
4+
`@api-ts/io-ts-http` API specification.
5+
6+
**Signature:**
7+
8+
```typescript
9+
import express from 'express';
10+
import { ApiSpec } from '@api-ts/io-ts-http'; // Conceptual import
11+
import { TypedRouter } from './typed-router'; // Conceptual import of the return type
12+
import { TypedRouterOptions } from './configuration'; // Conceptual import
13+
14+
declare function createRouter<T extends ApiSpec>(
15+
apiSpec: T,
16+
options?: TypedRouterOptions<T>, // Global options/hooks
17+
): TypedRouter<T>; // Returns the specialized router object
18+
```
19+
20+
**Parameters:**
21+
22+
- `apiSpec` (`ApiSpec`): An API specification object created using
23+
`@api-ts/io-ts-http`'s `apiSpec` function. This defines the routes that you can attach
24+
to this router.
25+
- `options` (Optional `TypedRouterOptions<T>`): An optional object containing global
26+
configuration hooks for error handling and post-response actions. See
27+
[Configuration Options](./configuration) for details.
28+
29+
**Return Value:**
30+
31+
- `TypedRouter<T>`: A specialized Express Router instance. This object has methods (like
32+
`.get`, `.post`) that accept operation names from the `apiSpec` and provide augmented
33+
`req` and `res` objects to the handlers. See [`TypedRouter` Object](./typed-router)
34+
for details.
35+
36+
**Usage Example:**
37+
38+
```typescript
39+
import express from 'express';
40+
import { createRouter } from '@api-ts/typed-express-router';
41+
import { MyApi } from 'my-api-package'; // Your apiSpec import
42+
43+
const app = express();
44+
const typedRouter = createRouter(MyApi, {
45+
// Optional global configuration
46+
onDecodeError: (errs, req, res, next) => {
47+
res.status(400).json({ error: 'Invalid request format', details: errs });
48+
},
49+
});
50+
51+
app.use('/api', typedRouter); // Mount the typed router
52+
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Reference: @api-ts/typed-express-router
2+
3+
This section provides detailed technical descriptions of the functions, objects, types,
4+
and configuration options available in the `@api-ts/typed-express-router` package. Use
5+
this reference to understand the specific parameters, return values, and behavior of its
6+
components when integrating `@api-ts/io-ts-http` specifications with Express.
7+
8+
## Components
9+
10+
- [**`createRouter`**](./create-router): Creates a new, typed Express Router instance
11+
linked to an API specification.
12+
- [**`wrapRouter`**](./wrap-router): Wraps an existing Express Router instance, linking
13+
it to an API specification.
14+
- [**`TypedRouter` Object**](./typed-router): Describes the router object returned by
15+
`createRouter` and `wrapRouter`, detailing its route definition methods (`.get`,
16+
`.post`, `.getUnchecked`, etc.) and middleware usage (`.use`).
17+
- [**Augmented Request & Response**](./request-response): Explains the properties and
18+
methods added to the standard Express `req` (`req.decoded`) and `res`
19+
(`res.sendEncoded`) objects within typed route handlers.
20+
- [**Configuration Options**](./configuration): Details the configurable options for
21+
error handling (`onDecodeError`, `onEncodeError`), post-response actions
22+
(`afterEncodedResponseSent`), and route aliasing (`routeAliases`).
23+
- [**`TypedRequestHandler` Type**](./typed-request-handler): Describes the TypeScript
24+
helper type for defining route handlers with correctly inferred augmented request and
25+
response types.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Augmented Request & Response
2+
3+
When you use route handlers registered via a [`TypedRouter`](./typed-router) object
4+
(using methods like `.get`, `.post`, `.getUnchecked`, etc.), the standard Express
5+
`request` and `response` objects are augmented with additional properties and methods
6+
related to the API specification.
7+
8+
## Augmented Request (`req`)
9+
10+
The Express `request` object (`req`) passed to typed route handlers includes an
11+
additional property:
12+
13+
### `req.decoded`
14+
15+
- **Type (Checked Routes):** `DecodedRequest`
16+
- In handlers attached using the "checked" methods (such as `typedRouter.get(...)`),
17+
`req.decoded` holds the successfully decoded and validated request data. Its
18+
TypeScript type is inferred directly from the `request` codec defined in the
19+
corresponding `httpRoute` of the `ApiSpec`. This object contains the flattened
20+
combination of path parameters, query parameters, headers, and body properties as
21+
defined by the `httpRequest` codec used in the spec.
22+
- **Type (Unchecked Routes & Middleware):** `Either<t.Errors, DecodedRequest>`
23+
- In handlers attached using the "unchecked" methods (such as
24+
`typedRouter.getUnchecked(...)`) or in middleware added via `typedRouter.use(...)`,
25+
`req.decoded` holds the raw result of the decoding attempt from `io-ts`. This is an
26+
`Either` type from the `fp-ts` library.
27+
- Use `E.isRight(req.decoded)` to check if decoding was successful. If true,
28+
`req.decoded.right` contains the `DecodedRequest`.
29+
- Use `E.isLeft(req.decoded)` to check if decoding failed. If true, `req.decoded.left`
30+
contains the `t.Errors` object detailing the validation failures.
31+
32+
## Augmented Response (`res`)
33+
34+
The Express `response` object (`res`) passed to typed route handlers includes an
35+
additional method:
36+
37+
### `res.sendEncoded(status, payload)`
38+
39+
Use this method instead of `res.json()` or `res.send()` when sending responses that
40+
should conform to the API specification.
41+
42+
**Parameters:**
43+
44+
- `status` (`number`): The HTTP status code for the response. This status code **must**
45+
be a key defined in the `response` object of the `httpRoute` associated with the
46+
current route in the `ApiSpec`.
47+
- `payload` (`any`): The data to be sent as the response body.
48+
49+
**Behavior:**
50+
51+
1. **Type Checking:** Validates that the provided `payload` conforms to the `io-ts`
52+
codec associated with the given `status` in the `httpRoute`'s `response` definition.
53+
2. **Encoding:** Encodes the `payload` using the same `io-ts` codec. This handles
54+
necessary transformations (such as converting a `Date` object to an ISO string if
55+
using `DateFromISOString`, or a `bigint` to a string if using `BigIntFromString`).
56+
3. **Sending Response:** Sets the response status code to `status`, sets the
57+
`Content-Type` header to `application/json`, and sends the JSON-stringified encoded
58+
payload as the response body.
59+
4. **Error Handling:** If the `payload` fails validation against the codec for the
60+
specified `status`, calls the `onEncodeError` hook (route-specific or global).
61+
5. **Post-Response Hook:** After the response has been successfully sent, calls the
62+
`afterEncodedResponseSent` hook (route-specific or global).
63+
64+
**Example:**
65+
66+
```typescript
67+
import { TypedRequestHandler } from '@api-ts/typed-express-router';
68+
import { MyApi } from 'my-api-package';
69+
70+
// Assuming 'api.v1.getUser' route expects a { user: UserType } payload for status 200
71+
const getUserHandler: TypedRequestHandler<MyApi['api.v1.getUser']['get']> = (
72+
req,
73+
res,
74+
) => {
75+
const userId = req.decoded.userId; // Access decoded request data
76+
const user = findUserById(userId);
77+
78+
if (!user) {
79+
// Assuming 404 is defined in the spec with an error object payload
80+
res.sendEncoded(404, { error: 'User not found' });
81+
return;
82+
}
83+
84+
// Send status 200 with the UserType payload
85+
// 'sendEncoded' ensures 'user' matches the spec for status 200
86+
res.sendEncoded(200, { user: user });
87+
};
88+
```
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# `TypedRequestHandler` Type
2+
3+
A TypeScript helper type provided by `@api-ts/typed-express-router` to help you define
4+
Express route handlers with correctly inferred types for the augmented `request` and
5+
`response` objects.
6+
7+
**Purpose:**
8+
9+
When defining handlers for "checked" routes (such as using `typedRouter.get(...)`), this
10+
type automatically infers:
11+
12+
- The type of `req.decoded` based on the `request` codec of the specific `httpRoute`
13+
linked via the `operationName`.
14+
- The type signature of `res.sendEncoded`, ensuring the `payload` type is checked
15+
against the appropriate `response` codec for the given `status` code from the
16+
`httpRoute`.
17+
18+
**Definition (Conceptual):**
19+
20+
```typescript
21+
import express from 'express';
22+
import { HttpRoute } from '@api-ts/io-ts-http'; // Conceptual import
23+
import * as t from 'io-ts'; // For TypeOf and OutputOf
24+
25+
// RouteDefinition represents the specific httpRoute object from the ApiSpec
26+
// e.g., MyApi['my.operation']['get']
27+
type RouteDefinition = HttpRoute<any, any>;
28+
29+
// Extracts the decoded request type from the route's request codec
30+
type DecodedRequest<R extends RouteDefinition> = t.TypeOf<R['request']>;
31+
32+
// Represents the augmented response object
33+
type TypedResponse<R extends RouteDefinition> = express.Response & {
34+
sendEncoded<Status extends keyof R['response'] & number>( // Status must be a key in response obj
35+
status: Status,
36+
// Payload type must match the codec for the given status
37+
payload: t.TypeOf<R['response'][Status]>,
38+
): TypedResponse<R>; // Allows chaining like standard Express res
39+
};
40+
41+
export type TypedRequestHandler<RouteDef extends RouteDefinition = any> = (
42+
req: express.Request & { decoded: DecodedRequest<RouteDef> },
43+
res: TypedResponse<RouteDef>,
44+
next: express.NextFunction,
45+
) => void | Promise<void>; // Allow async handlers
46+
```
47+
48+
(Note: The actual implementation may involve more complex generic constraints)
49+
50+
**Usage:** Import the type and use it when defining your handler functions. Provide the
51+
specific `httpRoute` definition type from your imported `ApiSpec` as the generic
52+
argument.
53+
54+
```typescript
55+
import express from 'express';
56+
import { TypedRequestHandler } from '@api-ts/typed-express-router';
57+
import { MyApi } from 'my-api-package'; // Your generated ApiSpec object
58+
59+
// Define the type for the specific route handler
60+
type HelloWorldRouteHandler = TypedRequestHandler<MyApi['hello.world']['get']>;
61+
// ^------------------------------^
62+
// Generic argument points to the specific httpRoute definition in the spec
63+
64+
const handler: HelloWorldRouteHandler = (req, res, next) => {
65+
// req.decoded is strongly typed based on MyApi['hello.world']['get'].request
66+
const name = req.decoded.name || 'World';
67+
68+
// Payload for status 200 is type-checked against MyApi['hello.world']['get'].response[200]
69+
res.sendEncoded(200, { message: `Hello, ${name}!` });
70+
71+
// If status 400 was defined in the spec with a different payload type:
72+
// const errorPayload = { error: 'Missing name' };
73+
// res.sendEncoded(400, errorPayload); // This would also be type-checked
74+
};
75+
76+
// Use the handler
77+
// typedRouter.get('hello.world', [handler]);
78+
```
79+
80+
Using `TypedRequestHandler` significantly improves your developer experience by
81+
providing type safety and autocompletion for the decoded request properties and the
82+
`sendEncoded` payload within route handlers.

0 commit comments

Comments
 (0)