From 1c2b000e1cc711742f6930c3209de262e69d0bcf Mon Sep 17 00:00:00 2001 From: Young Jun Joo Date: Thu, 24 Apr 2025 10:38:06 -0400 Subject: [PATCH 1/2] =?UTF-8?q?docs(superagent-wrapper):=20Restructure=20d?= =?UTF-8?q?ocumentation=20as=20Di=C3=A1taxis=20Reference=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit refactors the documentation for the `@api-ts/superagent-wrapper` package, migrating technical details from the main README into a dedicated, structured Reference section following the Diátaxis framework. **Motivation:** The previous documentation, while providing a getting started guide, lacked a formal, easily navigable technical reference for the package's core components. Users needing specific details about function signatures, parameters, return types, or the structure of the generated API client had to infer them from the narrative guide and examples. Adopting the Diátaxis framework provides a standardized, clear structure for technical information, improving user experience and maintainability. **Changes Implemented:** 1. **Established Diátaxis Reference Structure:** Created a new documentation structure under `docs/reference/superagent-wrapper/` specifically for reference material. 2. **Component-Based File Organization:** Split the reference documentation into multiple MDX files, each focusing on a distinct component or concept: * `index.mdx`: Provides an overview and entry point for the `@api-ts/superagent-wrapper` reference section. * `superagent-request-factory.mdx`: Contains the detailed technical reference for the `superagentRequestFactory` function. * `supertest-request-factory.mdx`: Contains the detailed technical reference for the `supertestRequestFactory` function. * `build-api-client.mdx`: Contains the detailed technical reference for the `buildApiClient` function. * `api-client.mdx`: Provides a comprehensive description of the structure of the `ApiClient` object returned by `buildApiClient`, including its operation methods, the `PreparedRequest` methods (`.decode()`, `.decodeExpecting()`), and the structure of the `ApiResponse`/`SpecificApiResponse` objects. 3. **Content Migration and Enhancement:** Extracted technical details and examples from the original README and rewrote them into formal reference documentation. This includes: * Precise descriptions of each function's purpose. * Clear representation of function signatures (including conceptual type definitions where applicable). * Detailed breakdown of parameters and return values. * Technical explanation of the generated `ApiClient` object's structure and behavior. * Explicit documentation of the `.decode()` and `.decodeExpecting()` methods and the response objects they return, including notes on type narrowing. **Benefits (Impact on Users):** * **Improved Findability:** Users can now directly navigate to the specific component they need information about (e.g., `buildApiClient`, `.decode()`). * **Enhanced Clarity:** Provides clear, unambiguous technical descriptions separate from narrative guides. * **Better Organization:** Structured according to a well-regarded documentation framework (Diátaxis). * **Increased Detail:** Offers more explicit information on signatures, types, and the behavior of the generated client than was previously available. * **Consistency:** Aligns the documentation approach with other packages potentially using the same framework (e.g., `@api-ts/io-ts-http`). This restructuring significantly improves the quality and usability of the technical documentation for `@api-ts/superagent-wrapper`. --- .../superagent-wrapper/api-client.md | 147 ++++++++++++++++++ .../superagent-wrapper/build-api-client.md | 83 ++++++++++ .../reference/superagent-wrapper/index.md | 41 +++++ .../superagent-request-factory.md | 62 ++++++++ .../supertest-request-factory.md | 60 +++++++ website/docusaurus.config.js | 2 +- website/sidebars.js | 14 ++ 7 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 website/docs/reference/superagent-wrapper/api-client.md create mode 100644 website/docs/reference/superagent-wrapper/build-api-client.md create mode 100644 website/docs/reference/superagent-wrapper/index.md create mode 100644 website/docs/reference/superagent-wrapper/superagent-request-factory.md create mode 100644 website/docs/reference/superagent-wrapper/supertest-request-factory.md 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/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..728b005c 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -78,6 +78,20 @@ 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', + ], + }, ], }, ], From ae5f7e416ce551823d518915f32a7ae85d8bffab Mon Sep 17 00:00:00 2001 From: Young Jun Joo Date: Thu, 24 Apr 2025 12:04:58 -0400 Subject: [PATCH 2/2] =?UTF-8?q?docs(typed-express-router):=20Refactor=20do?= =?UTF-8?q?cumentation=20into=20Di=C3=A1taxis=20Reference=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit significantly restructures and enhances the documentation for the `@api-ts/typed-express-router` package by adopting the Diátaxis framework and creating a dedicated, multi-file Reference section. **Motivation:** The previous documentation, primarily located in the README, mixed introductory guides with technical details. This made it difficult for users to quickly find precise information about specific functions, types, or configuration options without reading through narrative content. A formal Reference section, following Diátaxis principles, provides a much clearer, more accessible, and maintainable structure for technical documentation. **Changes Implemented:** 1. **Diátaxis Reference Structure:** Established a dedicated directory (`docs/reference/typed-express-router/`) for technical reference material, separating it from other documentation types (like Tutorials or How-To Guides, which might exist elsewhere). 2. **Component-Focused File Organization:** Divided the reference documentation into distinct MDX files, each dedicated to a specific component or concept of the library: * `index.mdx`: Serves as the entry point and overview for the typed-express-router reference section. * `create-router.mdx`: Details the `createRouter` function. * `wrap-router.mdx`: Details the `wrapRouter` function. * `typed-router.mdx`: Describes the `TypedRouter` object itself, including its typed route methods (e.g., `.get`, `.getUnchecked`) and middleware handling (`.use`). * `request-response.mdx`: Explains the augmented `req` (with `req.decoded`) and `res` (with `res.sendEncoded`) objects provided to route handlers. * `configuration.mdx`: Documents the global (`TypedRouterOptions`) and per-route (`RouteOptions`) configuration, including error hooks (`onDecodeError`, `onEncodeError`), the post-response hook (`afterEncodedResponseSent`), and `routeAliases`. * `typed-request-handler.mdx`: Describes the `TypedRequestHandler` helper type for improved type safety in handler definitions. 3. **Content Migration & Refinement:** Technical information previously embedded in the README's "Usage" section has been extracted, expanded, and reformatted into precise reference documentation within the new structure. This includes: * Clear function/type signatures. * Detailed descriptions of parameters and return values. * Explicit explanations of component behavior (e.g., checked vs. unchecked routes, hook triggering conditions). * Code examples focused on illustrating the specific component being documented. **Benefits (Impact on Users & Maintainers):** * **Improved Discoverability:** Users needing technical details about `createRouter`, `req.decoded`, configuration hooks, or other specific features can now find dedicated pages quickly. * **Enhanced Clarity & Precision:** Technical specifications are presented directly and unambiguously, separate from introductory narratives. * **Better Maintainability:** Isolating documentation for each component makes future updates easier and less error-prone. * **Standardized Structure:** Adopts a recognized documentation pattern (Diátaxis), improving consistency potentially across multiple related packages. * **Increased Comprehensiveness:** Provides more explicit detail on the library's features and types than previously available in the consolidated README. This refactoring represents a significant improvement in the quality, usability, and maintainability of the `@api-ts/typed-express-router` technical documentation. --- .../typed-express-router/configuration.md | 119 ++++++++++++++ .../typed-express-router/create-router.md | 52 ++++++ .../reference/typed-express-router/index.md | 25 +++ .../typed-express-router/request-response.md | 88 ++++++++++ .../typed-request-handler.md | 82 ++++++++++ .../typed-express-router/typed-router.md | 153 ++++++++++++++++++ .../typed-express-router/wrap-router.md | 60 +++++++ website/sidebars.js | 16 ++ 8 files changed, 595 insertions(+) create mode 100644 website/docs/reference/typed-express-router/configuration.md create mode 100644 website/docs/reference/typed-express-router/create-router.md create mode 100644 website/docs/reference/typed-express-router/index.md create mode 100644 website/docs/reference/typed-express-router/request-response.md create mode 100644 website/docs/reference/typed-express-router/typed-request-handler.md create mode 100644 website/docs/reference/typed-express-router/typed-router.md create mode 100644 website/docs/reference/typed-express-router/wrap-router.md 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/sidebars.js b/website/sidebars.js index 728b005c..b32a8f5a 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -92,6 +92,22 @@ const sidebars = { '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', + ], + }, ], }, ],