Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions website/docs/reference/typed-express-router/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Configuration Options

You can provide configuration options, primarily hooks for handling errors and
post-response actions, globally when creating/wrapping a router or on a per-route basis.
Per-route options override global ones.

## Global Options (`TypedRouterOptions`)

Passed as the optional second argument to [`createRouter`](./create-router) or the
optional third argument to [`wrapRouter`](./wrap-router).

```typescript
import express from 'express';
import * as t from 'io-ts'; // For Errors type
import { ApiSpec } from '@api-ts/io-ts-http'; // Conceptual

// Simplified representation of hook signatures
type DecodeErrorHandler<Req = any, Res = any> = (
errors: t.Errors,
req: express.Request & { decoded?: any }, // May not have decoded fully
res: express.Response & { sendEncoded?: any },
next: express.NextFunction,
) => void;

type EncodeErrorHandler<Req = any, Res = any> = (
error: unknown, // The error during encoding/validation
req: express.Request & { decoded?: any },
res: express.Response & { sendEncoded?: any },
next: express.NextFunction,
) => void;

type AfterResponseHandler<Req = any, Res = any> = (
status: number,
payload: any, // The successfully encoded payload
req: express.Request & { decoded?: any },
res: express.Response & { sendEncoded?: any },
) => void;

export type TypedRouterOptions<T extends ApiSpec = any> = {
onDecodeError?: DecodeErrorHandler;
onEncodeError?: EncodeErrorHandler;
afterEncodedResponseSent?: AfterResponseHandler;
};
```

- `onDecodeError(errors, req, res, next)`:
- **Triggered**: When using a "checked" route method (such as `.get`) and the incoming
request fails decoding or validation against the `httpRoute`'s `request` codec.
- **Purpose**: Allows custom formatting and sending of error responses (such as 400
Bad Request). If not provided, a default basic error handler might be used or the
error might propagate.
- `errors`: The `t.Errors` array from `io-ts` detailing the validation failures.
- **Note**: You typically end the response (`res.status(...).json(...).end()`) within
this handler. Calling `next()` might lead to unexpected behavior.
- `onEncodeError(error, req, res, next)`:
- **Triggered**: When `res.sendEncoded(status, payload)` is called, but the provided
`payload` fails validation against the `httpRoute`'s `response` codec for the given
`status`.
- **Purpose**: Handles server-side errors where the application tries to send data
inconsistent with the API specification. This usually indicates a bug.
- `error`: The validation error encountered.
- **Note**: You typically send a 500 Internal Server Error response here and should
end the response.
- `afterEncodedResponseSent(status, payload, req, res)`:
- **Triggered**: After `res.sendEncoded(status, payload)` has successfully validated,
encoded, and finished sending the response.
- **Purpose**: Lets you perform side-effects after a successful response, such as
logging, metrics collection, cleanup, etc.
- `status`: The status code that was sent.
- `payload`: The original (pre-encoding) payload object that was sent.
- **Note**: The response stream (`res`) is likely ended at this point. Don't attempt
to send further data.

## Per-Route Options (`RouteOptions`)

Pass these as the optional third argument to the route definition methods (such as
`typedRouter.get(..., ..., routeOptions)`).

```typescript
// RouteOptions includes the global hooks plus routeAliases
export type RouteOptions<RouteDef = any> = TypedRouterOptions & {
routeAliases?: string[];
};
```

- `onDecodeError` / `onEncodeError` / `afterEncodedResponseSent`: Same hooks as the
global options, but these versions apply only to the specific route they're defined on
and take precedence over any global hooks defined via `createRouter` or `wrapRouter`.
- `routeAliases` (`string[]`):
- An array of additional path strings that should also map to this route handler.
- Uses Express path syntax (such as `/path/:param`).
- See [`TypedRouter` Object](./typed-router) for more details and caveats regarding
path parameters.

## Example (Global and Per-Route):

```typescript
import { createRouter } from '@api-ts/typed-express-router';
import { MyApi } from 'my-api-package';

// Global options
const typedRouter = createRouter(MyApi, {
onDecodeError: globalDecodeErrorHandler,
afterEncodedResponseSent: globalMetricsHandler,
});

// Per-route options overriding global and adding alias
typedRouter.get('some.operation', [myHandler], {
routeAliases: ['/legacy/path'],
onDecodeError: specificDecodeErrorHandler, // Overrides globalDecodeErrorHandler for this route
// afterEncodedResponseSent is inherited from global options
});

typedRouter.post('another.operation', [otherHandler], {
// Inherits onDecodeError from global options
// No afterEncodedResponseSent hook will run for this route
afterEncodedResponseSent: undefined, // Explicitly disable global hook for this route
});
```
52 changes: 52 additions & 0 deletions website/docs/reference/typed-express-router/create-router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# `createRouter`

Creates a new Express Router instance that's typed according to a provided
`@api-ts/io-ts-http` API specification.

**Signature:**

```typescript
import express from 'express';
import { ApiSpec } from '@api-ts/io-ts-http'; // Conceptual import
import { TypedRouter } from './typed-router'; // Conceptual import of the return type
import { TypedRouterOptions } from './configuration'; // Conceptual import

declare function createRouter<T extends ApiSpec>(
apiSpec: T,
options?: TypedRouterOptions<T>, // Global options/hooks
): TypedRouter<T>; // Returns the specialized router object
```

**Parameters:**

- `apiSpec` (`ApiSpec`): An API specification object created using
`@api-ts/io-ts-http`'s `apiSpec` function. This defines the routes that you can attach
to this router.
- `options` (Optional `TypedRouterOptions<T>`): An optional object containing global
configuration hooks for error handling and post-response actions. See
[Configuration Options](./configuration) for details.

**Return Value:**

- `TypedRouter<T>`: A specialized Express Router instance. This object has methods (like
`.get`, `.post`) that accept operation names from the `apiSpec` and provide augmented
`req` and `res` objects to the handlers. See [`TypedRouter` Object](./typed-router)
for details.

**Usage Example:**

```typescript
import express from 'express';
import { createRouter } from '@api-ts/typed-express-router';
import { MyApi } from 'my-api-package'; // Your apiSpec import

const app = express();
const typedRouter = createRouter(MyApi, {
// Optional global configuration
onDecodeError: (errs, req, res, next) => {
res.status(400).json({ error: 'Invalid request format', details: errs });
},
});

app.use('/api', typedRouter); // Mount the typed router
```
25 changes: 25 additions & 0 deletions website/docs/reference/typed-express-router/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Reference: @api-ts/typed-express-router

This section provides detailed technical descriptions of the functions, objects, types,
and configuration options available in the `@api-ts/typed-express-router` package. Use
this reference to understand the specific parameters, return values, and behavior of its
components when integrating `@api-ts/io-ts-http` specifications with Express.

## Components

- [**`createRouter`**](./create-router): Creates a new, typed Express Router instance
linked to an API specification.
- [**`wrapRouter`**](./wrap-router): Wraps an existing Express Router instance, linking
it to an API specification.
- [**`TypedRouter` Object**](./typed-router): Describes the router object returned by
`createRouter` and `wrapRouter`, detailing its route definition methods (`.get`,
`.post`, `.getUnchecked`, etc.) and middleware usage (`.use`).
- [**Augmented Request & Response**](./request-response): Explains the properties and
methods added to the standard Express `req` (`req.decoded`) and `res`
(`res.sendEncoded`) objects within typed route handlers.
- [**Configuration Options**](./configuration): Details the configurable options for
error handling (`onDecodeError`, `onEncodeError`), post-response actions
(`afterEncodedResponseSent`), and route aliasing (`routeAliases`).
- [**`TypedRequestHandler` Type**](./typed-request-handler): Describes the TypeScript
helper type for defining route handlers with correctly inferred augmented request and
response types.
88 changes: 88 additions & 0 deletions website/docs/reference/typed-express-router/request-response.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Augmented Request & Response

When you use route handlers registered via a [`TypedRouter`](./typed-router) object
(using methods like `.get`, `.post`, `.getUnchecked`, etc.), the standard Express
`request` and `response` objects are augmented with additional properties and methods
related to the API specification.

## Augmented Request (`req`)

The Express `request` object (`req`) passed to typed route handlers includes an
additional property:

### `req.decoded`

- **Type (Checked Routes):** `DecodedRequest`
- In handlers attached using the "checked" methods (such as `typedRouter.get(...)`),
`req.decoded` holds the successfully decoded and validated request data. Its
TypeScript type is inferred directly from the `request` codec defined in the
corresponding `httpRoute` of the `ApiSpec`. This object contains the flattened
combination of path parameters, query parameters, headers, and body properties as
defined by the `httpRequest` codec used in the spec.
- **Type (Unchecked Routes & Middleware):** `Either<t.Errors, DecodedRequest>`
- In handlers attached using the "unchecked" methods (such as
`typedRouter.getUnchecked(...)`) or in middleware added via `typedRouter.use(...)`,
`req.decoded` holds the raw result of the decoding attempt from `io-ts`. This is an
`Either` type from the `fp-ts` library.
- Use `E.isRight(req.decoded)` to check if decoding was successful. If true,
`req.decoded.right` contains the `DecodedRequest`.
- Use `E.isLeft(req.decoded)` to check if decoding failed. If true, `req.decoded.left`
contains the `t.Errors` object detailing the validation failures.

## Augmented Response (`res`)

The Express `response` object (`res`) passed to typed route handlers includes an
additional method:

### `res.sendEncoded(status, payload)`

Use this method instead of `res.json()` or `res.send()` when sending responses that
should conform to the API specification.

**Parameters:**

- `status` (`number`): The HTTP status code for the response. This status code **must**
be a key defined in the `response` object of the `httpRoute` associated with the
current route in the `ApiSpec`.
- `payload` (`any`): The data to be sent as the response body.

**Behavior:**

1. **Type Checking:** Validates that the provided `payload` conforms to the `io-ts`
codec associated with the given `status` in the `httpRoute`'s `response` definition.
2. **Encoding:** Encodes the `payload` using the same `io-ts` codec. This handles
necessary transformations (such as converting a `Date` object to an ISO string if
using `DateFromISOString`, or a `bigint` to a string if using `BigIntFromString`).
3. **Sending Response:** Sets the response status code to `status`, sets the
`Content-Type` header to `application/json`, and sends the JSON-stringified encoded
payload as the response body.
4. **Error Handling:** If the `payload` fails validation against the codec for the
specified `status`, calls the `onEncodeError` hook (route-specific or global).
5. **Post-Response Hook:** After the response has been successfully sent, calls the
`afterEncodedResponseSent` hook (route-specific or global).

**Example:**

```typescript
import { TypedRequestHandler } from '@api-ts/typed-express-router';
import { MyApi } from 'my-api-package';

// Assuming 'api.v1.getUser' route expects a { user: UserType } payload for status 200
const getUserHandler: TypedRequestHandler<MyApi['api.v1.getUser']['get']> = (
req,
res,
) => {
const userId = req.decoded.userId; // Access decoded request data
const user = findUserById(userId);

if (!user) {
// Assuming 404 is defined in the spec with an error object payload
res.sendEncoded(404, { error: 'User not found' });
return;
}

// Send status 200 with the UserType payload
// 'sendEncoded' ensures 'user' matches the spec for status 200
res.sendEncoded(200, { user: user });
};
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# `TypedRequestHandler` Type

A TypeScript helper type provided by `@api-ts/typed-express-router` to help you define
Express route handlers with correctly inferred types for the augmented `request` and
`response` objects.

**Purpose:**

When defining handlers for "checked" routes (such as using `typedRouter.get(...)`), this
type automatically infers:

- The type of `req.decoded` based on the `request` codec of the specific `httpRoute`
linked via the `operationName`.
- The type signature of `res.sendEncoded`, ensuring the `payload` type is checked
against the appropriate `response` codec for the given `status` code from the
`httpRoute`.

**Definition (Conceptual):**

```typescript
import express from 'express';
import { HttpRoute } from '@api-ts/io-ts-http'; // Conceptual import
import * as t from 'io-ts'; // For TypeOf and OutputOf

// RouteDefinition represents the specific httpRoute object from the ApiSpec
// e.g., MyApi['my.operation']['get']
type RouteDefinition = HttpRoute<any, any>;

// Extracts the decoded request type from the route's request codec
type DecodedRequest<R extends RouteDefinition> = t.TypeOf<R['request']>;

// Represents the augmented response object
type TypedResponse<R extends RouteDefinition> = express.Response & {
sendEncoded<Status extends keyof R['response'] & number>( // Status must be a key in response obj
status: Status,
// Payload type must match the codec for the given status
payload: t.TypeOf<R['response'][Status]>,
): TypedResponse<R>; // Allows chaining like standard Express res
};

export type TypedRequestHandler<RouteDef extends RouteDefinition = any> = (
req: express.Request & { decoded: DecodedRequest<RouteDef> },
res: TypedResponse<RouteDef>,
next: express.NextFunction,
) => void | Promise<void>; // Allow async handlers
```

(Note: The actual implementation may involve more complex generic constraints)

**Usage:** Import the type and use it when defining your handler functions. Provide the
specific `httpRoute` definition type from your imported `ApiSpec` as the generic
argument.

```typescript
import express from 'express';
import { TypedRequestHandler } from '@api-ts/typed-express-router';
import { MyApi } from 'my-api-package'; // Your generated ApiSpec object

// Define the type for the specific route handler
type HelloWorldRouteHandler = TypedRequestHandler<MyApi['hello.world']['get']>;
// ^------------------------------^
// Generic argument points to the specific httpRoute definition in the spec

const handler: HelloWorldRouteHandler = (req, res, next) => {
// req.decoded is strongly typed based on MyApi['hello.world']['get'].request
const name = req.decoded.name || 'World';

// Payload for status 200 is type-checked against MyApi['hello.world']['get'].response[200]
res.sendEncoded(200, { message: `Hello, ${name}!` });

// If status 400 was defined in the spec with a different payload type:
// const errorPayload = { error: 'Missing name' };
// res.sendEncoded(400, errorPayload); // This would also be type-checked
};

// Use the handler
// typedRouter.get('hello.world', [handler]);
```

Using `TypedRequestHandler` significantly improves your developer experience by
providing type safety and autocompletion for the decoded request properties and the
`sendEncoded` payload within route handlers.
Loading
Loading