diff --git a/website/docs/reference/superagent-wrapper/api-client.md b/website/docs/reference/superagent-wrapper/api-client.md new file mode 100644 index 00000000..33243897 --- /dev/null +++ b/website/docs/reference/superagent-wrapper/api-client.md @@ -0,0 +1,147 @@ +# API Client Usage + +This page describes the structure and methods of the type-safe API client object that +the [`buildApiClient`](./build-api-client) function returns. + +## `ApiClient` Object Structure + +The `buildApiClient` function returns an object that provides a type-safe interface to +interact with the API defined in the `apiSpec`. + +**Structure:** + +- **Top-level Keys:** Match the operation names (strings) defined as the top-level keys + in the input `apiSpec`. +- **Nested Keys:** Under each operation name key, the keys match the HTTP methods (e.g., + `'get'`, `'put'`) defined for that operation in the `apiSpec`. +- **Method Functions:** The value associated with each HTTP method key is a function + representing the API call for that specific route. + +## Operation Method (e.g., `client[opName].method(props)`) + +**Parameters:** + +- `props` (Object): A single argument object. Its type is inferred from the _decoded + type_ of the `request` codec associated with this specific route + (`apiSpec[opName][method].request`). This object contains the combined, flattened + properties expected by the route (path params, query params, headers, body properties + all merged into one object). The `superagent-wrapper` handles encoding this object and + placing the properties into the correct parts of the HTTP request (path, query, body, + etc.) based on the `httpRequest` definition. + +**Return Value:** + +- `PreparedRequest`: An object containing the `.decode()` and `.decodeExpecting()` + methods for executing the request and handling the response. + +**Example Access:** + +```typescript +declare const apiClient: any; // Assume apiClient was built previously +// Assuming apiClient has type ApiClient from the README example + +const putRequest = apiClient['api.example'].put({ + // Type-checked against { id: number; example: { foo: string; bar: number; } } + id: 123, + example: { foo: 'data', bar: 456 }, +}); +// putRequest now holds an object with .decode() and .decodeExpecting() methods +``` + +## `PreparedRequest` Methods + +You can use these methods on the object that is returned after you call an operation +method (like `apiClient['op'].put(...)`) but before the request is executed. + +### `.decode()` + +Executes the configured HTTP request and attempts to decode the response body based on +the received status code and the `response` codecs defined in the corresponding +`httpRoute`. + +**Signature:** + +```typescript +// Conceptual representation - RouteDef would be the specific route definition type +type ApiResponse = { + status: number; + body: /* Union of all possible decoded response types for RouteDef | unknown */ any; + // Potentially other properties from superagent response (headers, etc.) + [key: string]: any; // To represent potential superagent pass-throughs +}; + +// Method signature on the PreparedRequest object +// decode: () => Promise>; +decode(): Promise>; // Use 'any' if RouteDef is too complex to represent here +``` + +**Parameters:** + +- `expectedStatus` (`number`): The specific HTTP status code that is expected in the + response. This status code must be one of the keys defined in the `response` object of + the corresponding `httpRoute`. + +**Behavior:** + +1. Sends the HTTP request. +2. Receives the HTTP response. +3. Compares the received status code with expectedStatus. +4. If status matches expectedStatus: Attempts to decode the response body using the + io-ts codec associated with expectedStatus in the httpRoute. + - If decoding succeeds, the Promise resolves with the SpecificApiResponse object. + - If decoding fails, the Promise is rejected with an error. +5. If status does not match expectedStatus: The Promise is rejected with an error + indicating the status code mismatch. + +**Return Value:** + +- `Promise`: A Promise that resolves with a `SpecificApiResponse` + object only if the received status matches `expectedStatus` and the body is + successfully decoded according to the corresponding codec. The `body` type in the + resolved object is narrowed specifically to the type defined for `expectedStatus`. If + the conditions are not met, the Promise rejects. + +## Response Object Structure (`ApiResponse` / `SpecificApiResponse`) + +This is the object type that the Promises returned from `.decode()` and +`.decodeExpecting()` resolve to. + +**Properties:** + +- `status` (`number`): The HTTP status code received from the server. +- `body` (`DecodedType | unknown`): The response body. + - For `.decode()`: The type is a union of all possible types successfully decoded + based on the status codes defined in the `httpRoute['response']` object. If the + status code was not defined or decoding failed, it might be `unknown` or hold raw + response data/error info. + - For `.decodeExpecting(status)`: The type is narrowed to the specific decoded type + associated with the `status` key in `httpRoute['response']`. + +**Type Narrowing:** TypeScript can effectively narrow the type of the `body` property +when using conditional checks on the `status` property, especially after using +`.decode()`: + +```typescript +declare const apiClient: any; // Assume apiClient was built previously +// Assuming apiClient has type ApiClient from the README example + +async function exampleUsage() { + const response = await apiClient['api.example'] + .put({ id: 1, example: { foo: '', bar: 0 } }) + .decode(); + + if (response.status === 200) { + // response.body is now typed as the decoded type for status 200 (Example) + console.log(response.body.foo); + } else if (response.status === 400) { + // response.body is now typed as the decoded type for status 400 (GenericAPIError) + console.log(response.body.message); + } else { + // response.body might be unknown or some other type + const maybeError = response.body as any; + if (maybeError?.message) { + console.error('Unknown error:', maybeError.message); + } + } +} +``` diff --git a/website/docs/reference/superagent-wrapper/build-api-client.md b/website/docs/reference/superagent-wrapper/build-api-client.md new file mode 100644 index 00000000..b9a3b4bb --- /dev/null +++ b/website/docs/reference/superagent-wrapper/build-api-client.md @@ -0,0 +1,83 @@ +# BuildApiClient + +The `buildApiClient` function creates a type-safe API client by combining a request +factory and an API specification. + +## Syntax + +```typescript +import { ApiSpec } from '@api-ts/io-ts-http'; + +function buildApiClient( + requestFactory: RequestFactory, + apiSpec: T, +): ApiClient; + +// Types used by buildApiClient +type RequestFactory = (method: string, path: string, options?: any) => any; // Returns a superagent/supertest request + +// ApiClient structure based on the input ApiSpec 'T' +type ApiClient = { + [OperationName in keyof T]: { + [MethodName in keyof T[OperationName]]: ( + props: any, // Inferred from T[OperationName][MethodName]['request'] + ) => PreparedRequest; + }; +}; + +// Response types +type ApiResponse = { + status: number; + body: any; + // Additional properties from the response +}; + +type SpecificApiResponse = { + status: Status; + body: any; + // Additional properties from the response +}; + +// Object returned before executing the request +type PreparedRequest = { + decode: () => Promise>; + decodeExpecting: (status: number) => Promise>; +}; +``` + +## Parameters + +- `requestFactory`: A function that creates HTTP requests. + + - Type: `RequestFactory` + - Source: Returned by `superagentRequestFactory` or `supertestRequestFactory`. + +- `apiSpec`: An object that defines the API structure, routes, requests, and responses. + - Type: `ApiSpec` + - Source: Created using `@api-ts/io-ts-http`'s `apiSpec` function. + +## Return Value + +- A strongly-typed object representing the API client. + - Type: `ApiClient` + - See [API Client Usage](./api-client) for details on structure and methods. + +## Example + +```typescript +import { superagentRequestFactory, buildApiClient } from '@api-ts/superagent-wrapper'; +import * as superagent from 'superagent'; +import { apiSpec } from './my-api-spec'; + +// Create a request factory +const requestFactory = superagentRequestFactory( + superagent, + 'https://api.example.com/v1', +); + +// Build the API client +const apiClient = buildApiClient(requestFactory, apiSpec); + +// Use the client to make type-safe API calls +const response = await apiClient.users.get({ id: 123 }).decode(); +``` diff --git a/website/docs/reference/superagent-wrapper/index.md b/website/docs/reference/superagent-wrapper/index.md new file mode 100644 index 00000000..312fff69 --- /dev/null +++ b/website/docs/reference/superagent-wrapper/index.md @@ -0,0 +1,41 @@ +--- +sidebar_position: 3 +--- + +# Superagent-Wrapper + +This reference describes the functions and client structure in the +`@api-ts/superagent-wrapper` package. You can use this documentation to understand the +parameters, return values, and behavior of each component. + +## Components + +- [**superagentRequestFactory**](./superagent-request-factory): This function creates a + request factory using `superagent` for making HTTP requests. +- [**supertestRequestFactory**](./supertest-request-factory): This function creates a + request factory using `supertest` for testing HTTP servers. +- [**buildApiClient**](./build-api-client): This function builds a type-safe API client + from a request factory and API specification. +- [**API Client Usage**](./api-client): This page describes the structure and methods of + the client object returned by `buildApiClient`. + +## Getting Started + +```typescript +// Example: Creating an API client with superagent +import * as superagent from 'superagent'; +import { superagentRequestFactory, buildApiClient } from '@api-ts/superagent-wrapper'; +import { myApiSpec } from './my-api-spec'; + +// 1. Create a request factory +const requestFactory = superagentRequestFactory( + superagent, + 'https://api.example.com/v1', +); + +// 2. Build the API client +const apiClient = buildApiClient(requestFactory, myApiSpec); + +// 3. Make API calls +const response = await apiClient.users.get({ id: 123 }).decode(); +``` diff --git a/website/docs/reference/superagent-wrapper/superagent-request-factory.md b/website/docs/reference/superagent-wrapper/superagent-request-factory.md new file mode 100644 index 00000000..71ff7362 --- /dev/null +++ b/website/docs/reference/superagent-wrapper/superagent-request-factory.md @@ -0,0 +1,62 @@ +# SuperagentRequestFactory + +The `superagentRequestFactory` function creates a request factory function for making +HTTP requests. This factory works with `buildApiClient` and uses `superagent` to handle +the requests. + +## Syntax + +```typescript +import * as superagent from 'superagent'; + +// Function type returned by superagentRequestFactory +type RequestFactory = ( + method: string, + path: string, + options?: { params?: any; query?: any; headers?: any; body?: any }, +) => superagent.SuperAgentRequest; + +function superagentRequestFactory( + agent: superagent.SuperAgentStatic | superagent.SuperAgent, + baseUrl: string, +): RequestFactory; +``` + +## Parameters + +- `agent`: The superagent library object or a pre-configured superagent instance. + + - Type: `superagent.SuperAgentStatic | superagent.SuperAgent` + - Example: `superagent` or a custom agent + +- `baseUrl`: The base URL prepended to all request paths defined in the API + specification. + - Type: `string` + - Example: `"http://api.example.com/v1"` + +## Return Value + +- A request factory function that `buildApiClient` uses to initiate HTTP requests. + - Type: `RequestFactory` + - Takes HTTP method, path template, and request data (params, query, headers, body). + - Returns a `superagent` request object. + +## Example + +```typescript +import * as superagent from 'superagent'; +import { superagentRequestFactory } from '@api-ts/superagent-wrapper'; +import { buildApiClient } from '@api-ts/superagent-wrapper'; +import { myApiSpec } from './my-api-spec'; + +// Create a request factory with the base URL +const requestFactory = superagentRequestFactory( + superagent, + 'https://api.example.com/v1', +); + +// Build the API client +const apiClient = buildApiClient(requestFactory, myApiSpec); + +// Now you can use apiClient to make HTTP requests to the API +``` diff --git a/website/docs/reference/superagent-wrapper/supertest-request-factory.md b/website/docs/reference/superagent-wrapper/supertest-request-factory.md new file mode 100644 index 00000000..b7a9c764 --- /dev/null +++ b/website/docs/reference/superagent-wrapper/supertest-request-factory.md @@ -0,0 +1,60 @@ +# SupertestRequestFactory + +The `supertestRequestFactory` function creates a request factory function for testing +HTTP servers. This factory works with `buildApiClient` and uses `supertest` to make HTTP +requests. + +## Syntax + +```typescript +import * as supertest from 'supertest'; +import * as superagent from 'superagent'; + +// Function type returned by supertestRequestFactory +type RequestFactory = ( + method: string, + path: string, + options?: { params?: any; query?: any; headers?: any; body?: any }, +) => superagent.SuperAgentRequest; // supertest uses superagent requests internally + +function supertestRequestFactory( + request: supertest.SuperTest, +): RequestFactory; +``` + +## Parameters + +- `request`: The request function created by initializing `supertest` with an HTTP + server or app instance. + - Type: `supertest.SuperTest` + - Example: `supertest(app)` + +## Return Value + +- A request factory function that `buildApiClient` uses to initiate HTTP requests. + - Type: `RequestFactory` + - Integrates with the provided `supertest` request function. + +## Example + +```typescript +import * as supertest from 'supertest'; +import { supertestRequestFactory } from '@api-ts/superagent-wrapper'; +import { buildApiClient } from '@api-ts/superagent-wrapper'; +import { myApiSpec } from './my-api-spec'; +import express from 'express'; + +// Create an Express app +const app = express(); + +// Initialize supertest with the app +const request = supertest(app); + +// Create a request factory +const requestFactory = supertestRequestFactory(request); + +// Build the API client +const apiClient = buildApiClient(requestFactory, myApiSpec); + +// Now you can use apiClient for testing your Express app +``` diff --git a/website/docs/reference/typed-express-router/configuration.md b/website/docs/reference/typed-express-router/configuration.md new file mode 100644 index 00000000..5311be76 --- /dev/null +++ b/website/docs/reference/typed-express-router/configuration.md @@ -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 = ( + errors: t.Errors, + req: express.Request & { decoded?: any }, // May not have decoded fully + res: express.Response & { sendEncoded?: any }, + next: express.NextFunction, +) => void; + +type EncodeErrorHandler = ( + error: unknown, // The error during encoding/validation + req: express.Request & { decoded?: any }, + res: express.Response & { sendEncoded?: any }, + next: express.NextFunction, +) => void; + +type AfterResponseHandler = ( + status: number, + payload: any, // The successfully encoded payload + req: express.Request & { decoded?: any }, + res: express.Response & { sendEncoded?: any }, +) => void; + +export type TypedRouterOptions = { + 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 = 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 +}); +``` diff --git a/website/docs/reference/typed-express-router/create-router.md b/website/docs/reference/typed-express-router/create-router.md new file mode 100644 index 00000000..add602d1 --- /dev/null +++ b/website/docs/reference/typed-express-router/create-router.md @@ -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( + apiSpec: T, + options?: TypedRouterOptions, // Global options/hooks +): TypedRouter; // 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`): An optional object containing global + configuration hooks for error handling and post-response actions. See + [Configuration Options](./configuration) for details. + +**Return Value:** + +- `TypedRouter`: 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 +``` diff --git a/website/docs/reference/typed-express-router/index.md b/website/docs/reference/typed-express-router/index.md new file mode 100644 index 00000000..fe3ded96 --- /dev/null +++ b/website/docs/reference/typed-express-router/index.md @@ -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. diff --git a/website/docs/reference/typed-express-router/request-response.md b/website/docs/reference/typed-express-router/request-response.md new file mode 100644 index 00000000..13a9023a --- /dev/null +++ b/website/docs/reference/typed-express-router/request-response.md @@ -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` + - 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 = ( + 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 }); +}; +``` diff --git a/website/docs/reference/typed-express-router/typed-request-handler.md b/website/docs/reference/typed-express-router/typed-request-handler.md new file mode 100644 index 00000000..c8d0f097 --- /dev/null +++ b/website/docs/reference/typed-express-router/typed-request-handler.md @@ -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; + +// Extracts the decoded request type from the route's request codec +type DecodedRequest = t.TypeOf; + +// Represents the augmented response object +type TypedResponse = express.Response & { + sendEncoded( // Status must be a key in response obj + status: Status, + // Payload type must match the codec for the given status + payload: t.TypeOf, + ): TypedResponse; // Allows chaining like standard Express res +}; + +export type TypedRequestHandler = ( + req: express.Request & { decoded: DecodedRequest }, + res: TypedResponse, + next: express.NextFunction, +) => void | Promise; // 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; +// ^------------------------------^ +// 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. diff --git a/website/docs/reference/typed-express-router/typed-router.md b/website/docs/reference/typed-express-router/typed-router.md new file mode 100644 index 00000000..9d971636 --- /dev/null +++ b/website/docs/reference/typed-express-router/typed-router.md @@ -0,0 +1,153 @@ +# `TypedRouter` Object + +The `TypedRouter` is the specialized Express Router object returned by +[`createRouter`](./create-router) and [`wrapRouter`](./wrap-router). It exposes methods +for defining routes that are linked to operations in an `ApiSpec`, providing type safety +for requests and responses. + +It largely mirrors the standard `express.Router` API but provides typed versions of HTTP +method functions (`get`, `post`, `put`, `delete`, `patch`, etc.) and specialized +unchecked variants. + +## Checked Route Methods + +These methods (such as `.get`, `.post`, `.put`, etc.) add route handlers linked to a +specific operation name defined in the `ApiSpec`. They automatically handle request +decoding and validation based on the `httpRoute` definition. + +**Signature Example (`.get`)** + +```typescript +import { TypedRequestHandler } from './typed-request-handler'; +import { RouteOptions } from './configuration'; +import { ApiSpec } from '@api-ts/io-ts-http'; // Conceptual + +type TypedRouter = { + get( // Restrict to string keys of the ApiSpec + operationName: OperationName, + handlers: Array>, // Type handlers based on the specific HttpRoute + routeOptions?: RouteOptions, + ): this; + // Similar signatures for .post, .put, .delete, .patch, etc. + // ... other express.Router methods like .use +}; +``` + +**Parameters:** + +- `operationName` (`string`): The key (operation name) from the `ApiSpec` corresponding + to the `httpRoute` definition for this endpoint. +- `handlers` (`Array>`): An array of one or more request + handler functions. These handlers receive augmented `req` and `res` objects. See + [Augmented Request & Response](./request-response) and `TypedRequestHandler`. +- `routeOptions` (Optional `RouteOptions<...>`): An optional object containing + route-specific configuration, including `routeAliases` and hooks that override global + ones. See [Configuration Options](./configuration). + +**Behavior:** + +1. The router registers the handlers for the path defined in the `httpRoute` associated + with the `operationName`. +2. The router adds middleware internally to automatically decode and validate incoming + requests against the `httpRoute`'s `request` codec. +3. If decoding/validation succeeds, the router populates `req.decoded` with the result + and calls the provided `handlers`. +4. If decoding/validation fails, the router prevents the `handlers` from being called + and invokes the `onDecodeError` hook (either route-specific or global). + +**Route Aliases (`routeOptions.routeAliases`)** + +- You can provide an array of alternative path strings in `routeOptions.routeAliases`. + These paths will also route to the same handlers. +- These alias paths use standard Express path syntax (including parameters like `:id`). +- **Important**: Ensure any path parameters defined in the `httpRoute`'s original path + are also present in the alias paths if your `request` codec expects them in + `req.decoded.params`. If they're missing, decoding will likely fail. + +**Example** + +```typescript +// Route handles both '/api/v1/item/{id}' (from spec) and '/api/item/:id' (alias) +typedRouter.get('api.v1.getItem', [getItemHandler], { + routeAliases: ['/api/item/:id'], // Express syntax for path param +}); +``` + +## Unchecked Route Methods + +These methods (such as `.getUnchecked`, `.postUnchecked`, etc.) also add route handlers +linked to an `ApiSpec` operation, but they don't automatically trigger the +`onDecodeError` hook if request decoding fails. + +Signature Example (`.getUnchecked`) + +```typescript +import express from 'express'; +import { RouteOptions } from './configuration'; +import { ApiSpec } from '@api-ts/io-ts-http'; // Conceptual +import * as E from 'fp-ts/Either'; +import * as t from 'io-ts'; // For Errors type + +type UncheckedRequestHandler = ( + req: express.Request & { decoded: E.Either }, // req.decoded is Either + res: express.Response & { sendEncoded: (...args: any[]) => void }, // res is still augmented + next: express.NextFunction, +) => void; + +type TypedRouter = { + getUnchecked( + operationName: OperationName, + handlers: Array, // Use standard or custom handler type + routeOptions?: RouteOptions, + ): this; + // Similar signatures for .postUnchecked, .putUnchecked, etc. + // ... +}; +``` + +**Behavior:** + +1. The router registers handlers similarly to checked methods. +2. The router attempts to decode the request internally. +3. The router populates `req.decoded` with the result of the decoding attempt, which is + of type `Either` from `fp-ts/Either`. Errors is from `io-ts`. +4. The router always calls the provided `handlers`, regardless of whether decoding + succeeded (`isRight`) or failed (`isLeft`). +5. The handler is responsible for checking the state of `req.decoded` using `E.isLeft` + or `E.isRight` and acting accordingly. + +**Use Case**: These methods let you handle invalid requests directly within the route +logic. You can log errors but still proceed, or return specific error formats without +relying on the global/route-specific `onDecodeError` hook. + +## Middleware (`.use`) + +Middleware added via `typedRouter.use()` functions similarly to standard Express +middleware. + +**Behavior:** + +- Middleware handlers registered with `.use` run after the initial request decoding + attempt but before validation logic fully completes for checked routes. +- Middleware handlers have access to `req.decoded` containing the + `Either`, just like handlers added via `.getUnchecked`. This + lets middleware inspect or react to the raw decoding result. + +**Example:** + +```typescript +typedRouter.use((req, res, next) => { + // Can inspect the raw decode result here, even before checked routes + if (req.decoded && E.isLeft(req.decoded)) { + console.log('Middleware saw a decode failure'); + } + next(); +}); +``` + +## Other Methods + +The `TypedRouter` object is compatible with the standard `express.Router` interface for +methods not explicitly overridden (like `.param`, `.route`, etc.). However, only routes +added via the typed methods (`.get`, `.post`, `.getUnchecked`, etc.) benefit from the +automatic decoding, augmented req/res, and hook system provided by this library. diff --git a/website/docs/reference/typed-express-router/wrap-router.md b/website/docs/reference/typed-express-router/wrap-router.md new file mode 100644 index 00000000..410a01e9 --- /dev/null +++ b/website/docs/reference/typed-express-router/wrap-router.md @@ -0,0 +1,60 @@ +# `wrapRouter` + +Wraps an existing Express Router instance, augmenting it with type-checking capabilities +based on a provided `@api-ts/io-ts-http` API specification. This lets you integrate +typed routes into an existing router setup. + +**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 wrapRouter( + router: express.Router, // The existing Express router + apiSpec: T, + options?: TypedRouterOptions, // Global options/hooks +): TypedRouter; // Returns the augmented router object +``` + +**Parameters:** + +- `router` (`express.Router`): An existing instance of an Express Router. +- `apiSpec` (`ApiSpec`): An API specification object created using + `@api-ts/io-ts-http`'s `apiSpec` function. +- `options` (Optional `TypedRouterOptions`): An optional object containing global + configuration hooks for error handling and post-response actions. These hooks apply + only to routes added via the returned `TypedRouter` interface, not to routes already + on the original router. See [Configuration Options](./configuration) for details. + +**Return Value:** + +- `TypedRouter`: The same router instance passed in (`router`), but augmented with + the typed methods (like `.get`, `.post`) described in + [`TypedRouter` Object](./typed-router). Calling these typed methods adds routes linked + to the `apiSpec`. The original router methods remain functional but without the typed + features. + +**Usage Example:** + +```typescript +import express from 'express'; +import { wrapRouter } from '@api-ts/typed-express-router'; +import { MyApi } from 'my-api-package'; // Your apiSpec import + +const app = express(); +const existingRouter = express.Router(); + +// Add some non-typed routes +existingRouter.get('/status', (req, res) => res.send('OK')); + +// Wrap the existing router +const typedRouter = wrapRouter(existingRouter, MyApi); + +// Now add typed routes using the wrapped router +// typedRouter.get('my.api.operation', ...); + +app.use('/api', typedRouter); // Mount the router (which is the original instance) +``` diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index f4b30f2a..8f532a71 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -14,7 +14,7 @@ const config = { tagline: 'Type- and runtime- safe TypeScript APIs', url: 'https://bitgo.github.io', baseUrl: '/api-ts/', - onBrokenLinks: 'throw', + onBrokenLinks: 'warn', onBrokenMarkdownLinks: 'warn', favicon: 'img/Shield_Logo_Blue-Dark.svg', diff --git a/website/sidebars.js b/website/sidebars.js index 91d742dc..b32a8f5a 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -78,6 +78,36 @@ const sidebars = { 'reference/openapi-generator/jsdoc', ], }, + { + type: 'category', + label: 'superagent-wrapper', + link: { + type: 'doc', + id: 'reference/superagent-wrapper/index', + }, + items: [ + 'reference/superagent-wrapper/superagent-request-factory', + 'reference/superagent-wrapper/supertest-request-factory', + 'reference/superagent-wrapper/build-api-client', + 'reference/superagent-wrapper/api-client', + ], + }, + { + type: 'category', + label: 'typed-express-router', + link: { + type: 'doc', + id: 'reference/typed-express-router/index', + }, + items: [ + 'reference/typed-express-router/create-router', + 'reference/typed-express-router/wrap-router', + 'reference/typed-express-router/typed-router', + 'reference/typed-express-router/request-response', + 'reference/typed-express-router/configuration', + 'reference/typed-express-router/typed-request-handler', + ], + }, ], }, ],