From 2b355613e34f1f55367043f4437548115915ad59 Mon Sep 17 00:00:00 2001 From: Young Jun Joo Date: Fri, 4 Apr 2025 12:11:07 -0400 Subject: [PATCH 1/3] =?UTF-8?q?Migrate=20io-ts-http=20documentation=20to?= =?UTF-8?q?=20Docusaurus=20website=20following=20Di=C3=A1taxis=20framework?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update documentation to follow Microsoft Writing Style Guide chore: prettier Remove io-ts-http-documentation-migration-plan.md --- packages/io-ts-http/README.md | 375 ++---------------- .../docs/explanation/io-ts-http-concepts.md | 112 ++++++ .../how-to-guides/advanced-http-routes.md | 234 +++++++++++ .../how-to-guides/working-with-combinators.md | 280 +++++++++++++ website/docs/reference/index.md | 14 + website/docs/reference/io-ts-http/api-spec.md | 50 +++ .../docs/reference/io-ts-http/combinators.md | 107 +++++ .../docs/reference/io-ts-http/http-request.md | 178 +++++++++ .../docs/reference/io-ts-http/http-route.md | 198 +++++++++ website/docs/reference/io-ts-http/index.md | 56 +++ 10 files changed, 1267 insertions(+), 337 deletions(-) create mode 100644 website/docs/explanation/io-ts-http-concepts.md create mode 100644 website/docs/how-to-guides/advanced-http-routes.md create mode 100644 website/docs/how-to-guides/working-with-combinators.md create mode 100644 website/docs/reference/index.md create mode 100644 website/docs/reference/io-ts-http/api-spec.md create mode 100644 website/docs/reference/io-ts-http/combinators.md create mode 100644 website/docs/reference/io-ts-http/http-request.md create mode 100644 website/docs/reference/io-ts-http/http-route.md create mode 100644 website/docs/reference/io-ts-http/index.md diff --git a/packages/io-ts-http/README.md b/packages/io-ts-http/README.md index 7f3d9e1b..9e5026c5 100644 --- a/packages/io-ts-http/README.md +++ b/packages/io-ts-http/README.md @@ -1,178 +1,31 @@ # @api-ts/io-ts-http -Runtime types for (de)serializing HTTP requests from both the client and server side - -## Contents - -- [@api-ts/io-ts-http](#api-tsio-ts-http) - - [Contents](#contents) - - [Preface](#preface) - - [Introduction](#introduction) - - [Overview](#overview) - - [Example](#example) - - [`apiSpec`](#apispec) - - [`httpRoute`](#httproute) - - [`path`](#path) - - [`method`](#method) - - [`request`](#request) - - [`response`](#response) - - [`httpRequest`](#httprequest) - - [`params`](#params) - - [`query`](#query) - - [`headers`](#headers) - - [`body`](#body) - - [Decoding an `httpRequest`](#decoding-an-httprequest) - - [Documentation](#documentation) - -## Preface - -This package extends [io-ts](https://github.com/gcanti/io-ts) with functionality useful -for typing HTTP requests. Start there for base knowledge required to use this package. +Runtime types for serializing and deserializing HTTP requests from both client and +server sides. ## Introduction -io-ts-http is the definition language for api-ts specifications, which define the API -contract for a web sever to an arbitrary degree of precision. Web servers can use the -io-ts-http spec to parse HTTP requests at runtime, and encode HTTP responses. Clients -can use the io-ts-http spec to enforce API compatibility at compile time, and to encode -requests to the server and decode responses. - -## Overview - -The primary function in this library is `httpRequest`. You can use this to build codecs -which can parse a generic HTTP request into a more refined type. The generic HTTP -request should conform to the following interface: - -```typescript -interface GenericHttpRequest { - params: { - [K: string]: string; - }; - query: { - [K: string]: string | string[]; - }; - headers: { - [K: string]: string; - }; - body?: unknown; -} -``` - -Here, `params` represents the path parameters and `query` is minimally-parsed query -string parameters (basically just the results of splitting up the query string and -urlDecoding the values). The `httpRequest` function can be combined with codecs from -`io-ts` to build a combined codec that is able to validate, parse, and encode these -generic HTTP requests into a more refined object. For example: - -```typescript -import { httpRequest, optional } from '@api-ts/io-ts-http'; -import { DateFromISOString, NumberFromString } from 'io-ts-types'; - -const ExampleHttpRequest = httpRequest({ - query: { - id: NumberFromString, - time: optional(DateFromISOString), - }, -}); -``` - -This builds a codec that can be given an arbitrary HTTP request and will ensure that it -contains an `id` parameter, and also optionally will check for a `time` parameter, and -if it is present, validate and parse it to a `Date`. If decoding succeeds, then the -resulting value's type will be: +Use io-ts-http as the definition language for api-ts specifications. It helps you define +precise API contracts for web servers. Web servers use the io-ts-http spec to parse HTTP +requests at runtime and encode HTTP responses. Clients use the io-ts-http spec to +enforce API compatibility at compile time and handle request/response encoding and +decoding. -```typescript -type ExampleDecodedResult = { - id: number; - time?: Date; -}; -``` - -This type is properly inferred by TypeScript and can be used in destructuring like so: - -```typescript -import { pipe } from 'fp-ts/function'; -import * as E from 'fp-ts/Either'; +## Installation -const { id, time } = pipe( - ExampleHttpRequest.decode(request), - E.getOrElseW((decodeErrors) => { - someErrorHandler(decodeErrors); - }), -); +```bash +npm install @api-ts/io-ts-http io-ts ``` -to get request argument validation and parsing as a one-liner. These codecs can also be -used from the client-side to get the type safety around making outgoing requests. An API -client could hypothetically have a method like: - -```typescript -apiClient.request(route, ExampleHttpRequest, { - id: 1337, - time: new Date(), -}); -``` - -If both the server and client use the same codec for the request, then it becomes -possible to encode the API contract (or at least as much of it that is possible to -express in the type system) and therefore someone calling the API can be confident that -the server will correctly interpret a request if the arguments typecheck. - -## Example - -Let's define the api-ts spec for a hypothetical `message-user` service. The conventional -top-level export is an -[`apiSpec`](https://github.com/BitGo/api-ts/blob/master/packages/io-ts-http/docs/apiSpec.md) -value; for example: - -### `apiSpec` - -```typescript -import { apiSpec } from '@api-ts/io-ts-http'; - -import { GetMessage, CreateMessage } from './routes/message'; -import { GetUser, CreateUser, PatchUser, UpdateUser, DeleteUser } from './routes/user'; - -/** - * message-user service - * - * @version 1.0.0 - */ -export const API = apiSpec({ - 'api.v1.message': { - get: GetMessage, - post: CreateMessage, - }, - 'api.v1.user': { - get: GetUser, - post: CreateUser, - put: UpdateUser, - delete: DeleteUser, - patch: PatchUser, - }, -}); -``` - -The `apiSpec` is imported, along with some named `httpRoute`s (`{Get|Create}Message`, -and `{Get|Create|Update|Delete}User`) [which we'll discuss below](#httproute). - -> Currently, if you add the `@version` JSDoc tag to the exported API spec, it will be -> used as the API `version` when generating an OpenAPI schema. Support for other tags -> may be added in the future. - -The top-level export for `message-user-types` is `API`, which we define as an `apiSpec` -with two endpoints `api/v1/message` and `api/v1/user`. The `api/v1/message` endpoint -responds to `GET` and `POST` verbs while the second reponds to `GET`, `POST`, `PUT`, and -`DELETE` verbs using `httpRoute`s defined in `./routes/message`. The following are the -`httpRoute`s defined in `./routes/message`. - -### `httpRoute` +## Quick example ```typescript import * as t from 'io-ts'; -import { httpRoute, httpRequest } from '@api-ts/io-ts-http'; +import { apiSpec, httpRoute, httpRequest } from '@api-ts/io-ts-http'; +import { NumberFromString } from 'io-ts-types'; -export const GetMessage = httpRoute({ +// Create a route for getting a message by ID +const GetMessage = httpRoute({ path: '/message/{id}', method: 'GET', request: httpRequest({ @@ -191,192 +44,40 @@ export const GetMessage = httpRoute({ }, }); -export const CreateMessage = httpRoute({ - path: '/message', - method: 'POST', - request: httpRequest({ - body: { - message: t.string, - }, - }), - response: { - 200: t.type({ - id: t.string, - message: t.string, - }), - 404: t.type({ - error: t.string, - }), - }, -}); -``` - -The first import is the `io-ts` package. It's usually imported `as t` for use in -describing the types of data properties. Again, review -[io-ts](https://github.com/gcanti/io-ts) documentation for more context on how to use it -and this package. - -Then `httpRoute` and `httpRequest` are imported. We'll review the -[`httpRequest`](#httprequest) below, but first, let's review the `GetMessage` -`httpRoute`. - -```typescript -export const GetMessage = httpRoute({ - path: '/message/{id}', - method: 'GET', - request: httpRequest({ - params: { - id: t.string, - }, - }), - response: { - 200: t.type({ - id: t.string, - message: t.string, - }), - 404: t.type({ - error: t.string, - }), +// Define your API specification +export const API = apiSpec({ + 'api.v1.message': { + get: GetMessage, }, }); ``` -[`httpRoute`](https://github.com/BitGo/api-ts/blob/master/packages/io-ts-http/docs/httpRoute.md)s -`apiSpec`s use -[`httpRoute`](https://github.com/BitGo/api-ts/blob/master/packages/io-ts-http/docs/httpRoute.md)s -to define the `path`, `method`, `request` and `response` of a route. - -#### `path` - -The route's `path` along with possible path variables. You should surround variables -with brackets like `{name}`, and are to the `request` codec under the `params` property. - -#### `method` - -The route's `method` is the -[HTTP request method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) to use -for that route. In our `GetMessage` example, the `method` is `GET`, while in our -`PostMessage` example, the `method` is `POST`. - -#### `request` - -The route's `request` is the output of the `httpRequest` function. This will be -described below. - -#### `response` - -The route's `response` describes the possible -[HTTP responses](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) the route can -produce. The key-value pairs of the `response` object are an HTTP status code followed -by the `io-ts` type of the response body. In our `GetMessage` example, a `200` status -response yields a payload of a JSON object with two properties, `message` which is a -`string` and `id` which is also a `string`, and a `404` yeilds a payload of a JSON -object with a single property `error` which is a `String`. - -### `httpRequest` - -Use `httpRequest` to build the expected type that you pass in a request to the route. In -our example `GetMessage` - -```typescript -export const GetMessage = httpRoute({ - path: '/message/{id}', - method: 'GET', - request: httpRequest({ - params: { - id: t.string, - }, - }), - // ... -}); -``` - -`httpRequest`s have a total of 4 optional properties: `params` (shown in the example), -`query`, `headers`, and `body`. - -#### `params` - -`params` is an object representing the types of path parameters in a URL. Any URL -parameters in the `path` property of an `httpRoute` must be accounted for in the -`params` property of the `httpRequest`. Our request has a single URL parameter it is -expecting, `id`. This is the type of this parameter is captured in the `params` object -of our `httpRequest`. - -#### `query` - -`query` is the object representing the values passed in via query parameters at the end -of a URL. The following example uses a new route, `GetMessages`, to our API that -searches messages related to a specific `author`: - -```typescript -export const GetMessages = httpRoute({ - path: '/messages', - method: 'GET', - request: httpRequest({ - query: { - author: t.string, - }, - }), - // ... -}); -``` - -#### `headers` - -`headers` is an object representing the types of the -[HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) passed in with -a request. - -#### `body` +## Documentation -`body` is an object representing the type of the -[HTTP body](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#body) of a -request. Often this is a JSON object. The `CreateMessage` `httpRoute` in our example -uses the `body` property: +For comprehensive documentation, visit our +[official documentation site](https://bitgo.github.io/api-ts/docs/reference/io-ts-http): -```typescript -export const CreateMessage = httpRoute({ - path: '/message', - method: 'POST', - request: httpRequest({ - body: { - message: t.string, - }, - }), - // ... -}); -``` +- **Tutorials**: Step-by-step guides to get started -#### Decoding an `httpRequest` + - [Create an API specification](https://bitgo.github.io/api-ts/docs/tutorial-basics/create-an-api-spec) + - [Create an HTTP server](https://bitgo.github.io/api-ts/docs/tutorial-basics/create-an-http-server) + - [Create an HTTP client](https://bitgo.github.io/api-ts/docs/tutorial-basics/create-an-http-client) -When you decode `httpRequests` using `io-ts` helpers, the properties of the request are -flattened like this: +- **How-to guides**: Problem-oriented guides for specific tasks -```typescript -import { DateFromISOString, NumberFromString } from 'io-ts-types'; + - [Advanced HTTP routes](https://bitgo.github.io/api-ts/docs/how-to-guides/advanced-http-routes) + - [Working with combinators](https://bitgo.github.io/api-ts/docs/how-to-guides/working-with-combinators) -// build an httpRequest with one parameter id and a body with content and a timestamp -const Request = httpRequest({ - params: { - id: NumberFromString, - }, - body: { - content: t.string, - timestamp: DateFromISOString, - }, -}); +- **Explanation**: Conceptual documentation -// use io-ts to get the type of the Request -type Request = t.TypeOf; + - [Understanding io-ts-http](https://bitgo.github.io/api-ts/docs/explanation/io-ts-http-concepts) -// same as -type Request = { - id: number; - content: string; - timestamp: Date; -}; -``` +- **Reference**: Technical documentation + - [apiSpec](https://bitgo.github.io/api-ts/docs/reference/io-ts-http/api-spec) + - [httpRoute](https://bitgo.github.io/api-ts/docs/reference/io-ts-http/http-route) + - [httpRequest](https://bitgo.github.io/api-ts/docs/reference/io-ts-http/http-request) + - [combinators](https://bitgo.github.io/api-ts/docs/reference/io-ts-http/combinators) -## Documentation +## License -- [API Reference](https://github.com/BitGo/api-ts/blob/master/packages/io-ts-http/docs/apiReference.md) +MIT diff --git a/website/docs/explanation/io-ts-http-concepts.md b/website/docs/explanation/io-ts-http-concepts.md new file mode 100644 index 00000000..2c3d68da --- /dev/null +++ b/website/docs/explanation/io-ts-http-concepts.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 2 +--- + +# Understanding io-ts-http + +## Introduction + +Use `io-ts-http` as the definition language for api-ts specifications. It helps you +define precise API contracts for web servers. This article explains the key concepts and +design principles behind `io-ts-http`. + +## Core concepts + +### API contract as a type system + +Express your API contract through TypeScript's type system with runtime validation using +`io-ts`. This approach offers three main benefits: + +1. **Type safety**: Use the same types for both clients and servers to ensure + compatibility. +2. **Runtime validation**: Validate data at runtime, unlike TypeScript's types which are + erased during compilation. +3. **Parse, don't validate**: Not only validate input but also parse it into the correct + shape and types. + +### Building blocks + +`io-ts-http` has three main components: + +1. **`apiSpec`**: Create the top-level definition of your API and organize routes under + operation names. +2. **`httpRoute`**: Define individual routes with their paths, methods, request schemas, + and response schemas. +3. **`httpRequest`**: Specify the expected shape of HTTP requests, including parameters, + query strings, headers, and bodies. + +## How it works + +When you define an API with `io-ts-http`, you create a specification that works in +multiple ways: + +1. **Server-side**: Validate and parse incoming requests to ensure they match your API + contract. +2. **Client-side**: Ensure requests are properly formed before sending them to the + server. +3. **Documentation**: Generate API documentation such as OpenAPI specifications. + +The same specification works everywhere, eliminating discrepancies between your +implementation and documentation. + +### Property flattening + +Property flattening is an important concept in `io-ts-http`. When you decode HTTP +requests, the system flattens properties from different sources (path parameters, query +parameters, headers, and body) into a single object. + +For example, if you have: + +- A path parameter `id` +- A query parameter `filter` +- A body with properties `name` and `email` + +The decoded object becomes: + +```typescript +{ + id: string, + filter: string, + name: string, + email: string +} +``` + +This makes it easier to work with the data without accessing different parts of the +request separately. + +## Design principles + +### 1. Type safety first + +`io-ts-http` prioritizes type safety. It uses `io-ts` codecs to ensure your API +contracts are type-checked at compile time and validated at runtime. + +### 2. Separation of concerns + +The library separates: + +- Defining what your API looks like (`apiSpec` and `httpRoute`) +- Defining how requests and responses should be structured (`httpRequest`) +- Processing HTTP requests and responses (handled by other packages) + +### 3. Flexibility + +`io-ts-http` is flexible. It doesn't dictate how to implement your server or client—only +how to define your API contract. You can use it with any HTTP framework or client +library. + +## When to use io-ts-http + +Use `io-ts-http` when you need to: + +1. Build a TypeScript API with type safety between client and server +2. Validate complex request structures with nested objects, arrays, or optional fields +3. Generate documentation from your code to keep it current +4. Implement the "Parse, Don't Validate" pattern for robust error handling + +## Conclusion + +`io-ts-http` offers a powerful way to define type-safe API contracts for your entire +application stack. By centralizing your API definition in a single source of truth, you +can ensure consistency and reduce errors in your application. diff --git a/website/docs/how-to-guides/advanced-http-routes.md b/website/docs/how-to-guides/advanced-http-routes.md new file mode 100644 index 00000000..1c53da3b --- /dev/null +++ b/website/docs/how-to-guides/advanced-http-routes.md @@ -0,0 +1,234 @@ +# Advanced HTTP route patterns + +Learn advanced patterns for defining HTTP routes with `io-ts-http` beyond the basic +usage examples. + +## Work with non-object body types + +By default, `httpRequest` assumes the request body is a JSON object. Sometimes you need +to accept other types like strings, numbers, or arrays. + +### Accept a string body + +Use `t.intersection` to combine `httpRequest` with a custom type that accepts a string +body: + +```typescript +import * as t from 'io-ts'; +import { httpRoute, httpRequest } from '@api-ts/io-ts-http'; + +const StringBodyRoute = httpRoute({ + path: '/example/{id}', + method: 'POST', + request: t.intersection([ + httpRequest({ + params: { id: t.string }, + }), + t.type({ + body: t.string, + }), + ]), + response: { + 200: t.string, + }, +}); + +// Decoded type +type DecodedType = { + id: string; + body: string; +}; +``` + +This approach breaks the abstraction slightly by exposing a `body` property in the +decoded type, but it works effectively. + +### Accept an array body + +Similarly, accept an array body: + +```typescript +const ArrayBodyRoute = httpRoute({ + path: '/batch-process', + method: 'POST', + request: t.intersection([ + httpRequest({}), + t.type({ + body: t.array(t.string), + }), + ]), + response: { + 200: t.type({ + processed: t.number, + }), + }, +}); + +// Decoded type +type DecodedType = { + body: string[]; +}; +``` + +## Create conditional request parameters + +Sometimes you need parameters that are required only when other parameters have specific +values. + +### Use union types + +Use `t.union` with multiple `httpRequest` objects to handle conditional parameters: + +```typescript +const SearchRoute = httpRoute({ + path: '/search', + method: 'GET', + request: t.union([ + // When searching by keyword + httpRequest({ + query: { + type: t.literal('keyword'), + keyword: t.string, + }, + }), + // When searching by category + httpRequest({ + query: { + type: t.literal('category'), + categoryId: NumberFromString, + }, + }), + // When searching by both + httpRequest({ + query: { + type: t.literal('combined'), + keyword: t.string, + categoryId: NumberFromString, + }, + }), + ]), + response: { + 200: t.array( + t.type({ + id: t.string, + title: t.string, + }), + ), + }, +}); + +// Decoded type will be a union: +type DecodedType = + | { type: 'keyword'; keyword: string } + | { type: 'category'; categoryId: number } + | { type: 'combined'; keyword: string; categoryId: number }; +``` + +## Add optional headers + +HTTP headers are often optional. Use the `optional` combinator to define optional +headers: + +```typescript +import { httpRequest, optional } from '@api-ts/io-ts-http'; + +const RequestWithOptionalHeaders = httpRoute({ + path: '/resource', + method: 'GET', + request: httpRequest({ + headers: { + authorization: t.string, + 'cache-control': optional(t.string), + 'if-modified-since': optional(t.string), + }, + }), + response: { + 200: t.object, + }, +}); + +// Decoded type +type DecodedType = { + authorization: string; + 'cache-control'?: string; + 'if-modified-since'?: string; +}; +``` + +## Handle file uploads + +File uploads typically use `multipart/form-data` encoding. While `io-ts-http` doesn't +directly support file uploads, you can treat the file as an opaque object in the type +system and handle the file processing separately: + +```typescript +const FileUploadRoute = httpRoute({ + path: '/upload', + method: 'POST', + request: httpRequest({ + body: { + // In the type system, just indicate a file is expected + // Your server framework will handle the actual file + file: t.unknown, + description: optional(t.string), + }, + }), + response: { + 200: t.type({ + fileId: t.string, + size: t.number, + }), + }, +}); +``` + +## Combine multiple request sources + +Sometimes you need to extract information from multiple sources, such as getting an ID +from the path, authentication from headers, and data from the body: + +```typescript +const ComplexRoute = httpRoute({ + path: '/users/{userId}/profile', + method: 'PUT', + request: httpRequest({ + params: { + userId: NumberFromString, + }, + headers: { + authorization: t.string, + }, + body: { + name: t.string, + email: t.string, + preferences: t.type({ + theme: t.union([t.literal('light'), t.literal('dark')]), + notifications: t.boolean, + }), + }, + }), + response: { + 200: t.type({ + success: t.boolean, + }), + }, +}); + +// Decoded type +type DecodedType = { + userId: number; + authorization: string; + name: string; + email: string; + preferences: { + theme: 'light' | 'dark'; + notifications: boolean; + }; +}; +``` + +## Summary + +These advanced patterns help you define complex HTTP routes that accurately reflect your +API's requirements. By combining `io-ts` with `httpRequest` and `httpRoute`, you can +create type-safe APIs with sophisticated validation logic. diff --git a/website/docs/how-to-guides/working-with-combinators.md b/website/docs/how-to-guides/working-with-combinators.md new file mode 100644 index 00000000..c7407093 --- /dev/null +++ b/website/docs/how-to-guides/working-with-combinators.md @@ -0,0 +1,280 @@ +# Work with io-ts-http combinators + +Learn how to use combinators in `io-ts-http` to create more expressive and maintainable +API specifications. + +## Use the optional combinator + +Make specific properties optional in your request or response types with the `optional` +combinator. + +### Basic usage + +```typescript +import * as t from 'io-ts'; +import { httpRequest, optional } from '@api-ts/io-ts-http'; + +const UserSearchRequest = httpRequest({ + query: { + name: t.string, + age: optional(t.number), + city: optional(t.string), + }, +}); + +// Accepts requests with or without age and city parameters +// Decoded type: +// { +// name: string; +// age?: number; +// city?: string; +// } +``` + +### With nested objects + +```typescript +const ProductRequest = httpRequest({ + body: { + name: t.string, + price: t.number, + details: optional( + t.type({ + description: t.string, + dimensions: t.type({ + width: t.number, + height: t.number, + depth: t.number, + }), + }), + ), + }, +}); + +// Includes an optional details object in the decoded type +``` + +## Use the optionalize combinator + +Define object types with both required and optional properties easily using the +`optionalize` combinator. + +### Basic usage + +```typescript +import { optionalize } from '@api-ts/io-ts-http'; + +const UserProfile = optionalize({ + id: t.string, + name: t.string, + email: t.string, + phone: t.union([t.string, t.undefined]), // Optional + address: t.union([ + t.type({ + street: t.string, + city: t.string, + }), + t.undefined, + ]), // Optional +}); + +// Decoded type: +// { +// id: string; +// name: string; +// email: string; +// phone?: string; +// address?: { +// street: string; +// city: string; +// }; +// } +``` + +### Combine with optional + +Make your code more readable by using `optionalize` with `optional`: + +```typescript +const UserProfile = optionalize({ + id: t.string, + name: t.string, + email: t.string, + phone: optional(t.string), + address: optional( + t.type({ + street: t.string, + city: t.string, + }), + ), +}); + +// Same decoded type as above, but clearer intent +``` + +## Use the flattened combinator + +Decode nested objects into a flat structure, or encode flat objects into a nested +structure with the `flattened` combinator. + +### Basic usage + +```typescript +import { flattened } from '@api-ts/io-ts-http'; + +const NestedRequest = flattened({ + user: { + id: t.string, + name: t.string, + }, + metadata: { + createdAt: DateFromISOString, + updatedAt: DateFromISOString, + }, +}); + +// Input when encoding: +// { +// id: 'user123', +// name: 'John Doe', +// createdAt: new Date(), +// updatedAt: new Date(), +// } + +// Output after encoding: +// { +// user: { +// id: 'user123', +// name: 'John Doe', +// }, +// metadata: { +// createdAt: '2023-01-01T00:00:00.000Z', +// updatedAt: '2023-01-02T00:00:00.000Z', +// }, +// } +``` + +### With httpRequest + +Organize related properties in `httpRequest` using the `flattened` combinator: + +```typescript +const OrderRequest = httpRequest({ + params: { + orderId: t.string, + }, + body: flattened({ + customer: { + name: t.string, + email: t.string, + }, + shipping: { + address: t.string, + city: t.string, + zipCode: t.string, + }, + payment: { + method: t.union([t.literal('credit'), t.literal('debit'), t.literal('paypal')]), + amount: t.number, + }, + }), +}); + +// Decoded type: +// { +// orderId: string; +// name: string; +// email: string; +// address: string; +// city: string; +// zipCode: string; +// method: 'credit' | 'debit' | 'paypal'; +// amount: number; +// } +``` + +## Real-world examples + +### Create a user registration API + +```typescript +import * as t from 'io-ts'; +import { httpRoute, httpRequest, optional, optionalize } from '@api-ts/io-ts-http'; + +const RegisterUserRoute = httpRoute({ + path: '/users', + method: 'POST', + request: httpRequest({ + body: optionalize({ + username: t.string, + email: t.string, + password: t.string, + fullName: optional(t.string), + preferences: optional( + t.type({ + theme: t.union([t.literal('light'), t.literal('dark')]), + language: t.string, + }), + ), + }), + }), + response: { + 201: t.type({ + id: t.string, + username: t.string, + email: t.string, + }), + 400: t.type({ + error: t.string, + }), + }, +}); +``` + +### Create a search API with multiple parameters + +```typescript +const ProductSearchRoute = httpRoute({ + path: '/products/search', + method: 'GET', + request: httpRequest({ + query: optionalize({ + query: t.string, + category: optional(t.string), + minPrice: optional(NumberFromString), + maxPrice: optional(NumberFromString), + sort: optional( + t.union([ + t.literal('price_asc'), + t.literal('price_desc'), + t.literal('newest'), + t.literal('popular'), + ]), + ), + page: optional(NumberFromString), + limit: optional(NumberFromString), + }), + }), + response: { + 200: t.type({ + products: t.array( + t.type({ + id: t.string, + name: t.string, + price: t.number, + // Other product fields + }), + ), + total: t.number, + page: t.number, + pages: t.number, + }), + }, +}); +``` + +## Summary + +The combinators in `io-ts-http` help you define complex API specifications while keeping +your code readable and maintainable. Use these combinators to create type-safe API +contracts that accurately represent your application's requirements. diff --git a/website/docs/reference/index.md b/website/docs/reference/index.md new file mode 100644 index 00000000..a02bccbc --- /dev/null +++ b/website/docs/reference/index.md @@ -0,0 +1,14 @@ +--- +sidebar_position: 1 +--- + +# API Reference + +Find detailed technical documentation for api-ts components in this section. + +## io-ts-http + +- [apiSpec](./io-ts-http/api-spec): Define a collection of routes +- [httpRoute](./io-ts-http/http-route): Create individual HTTP routes +- [httpRequest](./io-ts-http/http-request): Build HTTP request codecs +- [combinators](./io-ts-http/combinators): Create more expressive types diff --git a/website/docs/reference/io-ts-http/api-spec.md b/website/docs/reference/io-ts-http/api-spec.md new file mode 100644 index 00000000..b12c2d24 --- /dev/null +++ b/website/docs/reference/io-ts-http/api-spec.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 1 +--- + +# apiSpec + +Define a collection of routes and associate them with operation names. + +## Usage + +Create a top-level export using `apiSpec` to define your service's API as a collection +of operations linked to routes. This function enforces the correct type for the +parameter you pass to it. + +```typescript +import { apiSpec } from '@api-ts/io-ts-http'; + +import { GetMessage, CreateMessage } from './routes/message'; +import { GetUser, CreateUser, PatchUser, UpdateUser, DeleteUser } from './routes/user'; + +/** + * Example service + * + * @version 1.0.0 + */ +export const API = apiSpec({ + 'api.v1.message': { + get: GetMessage, + post: CreateMessage, + }, + 'api.v1.user': { + get: GetUser, + post: CreateUser, + put: UpdateUser, + delete: DeleteUser, + patch: PatchUser, + }, +}); +``` + +Other packages can read these API specs to: + +- Create type-checked API clients +- Bind functions to routes with automatic parsing and validation +- Generate OpenAPI schemas + +## Metadata + +Add a `@version` JSDoc tag to the exported API spec to set the API version when +generating an OpenAPI schema. We may add support for more tags in the future. diff --git a/website/docs/reference/io-ts-http/combinators.md b/website/docs/reference/io-ts-http/combinators.md new file mode 100644 index 00000000..c5798ad3 --- /dev/null +++ b/website/docs/reference/io-ts-http/combinators.md @@ -0,0 +1,107 @@ +--- +sidebar_position: 4 +--- + +# Combinators + +Use these `io-ts-http` combinators with `io-ts` codecs to create more expressive types. + +## optionalize + +Create object types with both required and optional properties easily. This combinator +accepts the same properties as `type` and `partial` in `io-ts`, combining their +functionality. It automatically identifies properties that can be `undefined` and marks +them as optional. + +### Example + +```typescript +const Item = optionalize({ + necessaryProperty: t.string, + maybeDefined: t.union([t.string, t.undefined]), +}); +``` + +This creates a codec for: + +```typescript +type Item = { + necessaryProperty: string; + maybeDefined?: string; +}; +``` + +You could define this same type using a combination of `intersection`, `type`, and +`partial`, but `optionalize` is more readable, especially when combined with the +`optional` combinator. + +## optional + +Make any codec optional by combining it with `undefined`. This combinator works well +with `optionalize` to clearly show which parameters are optional. + +### Example + +```typescript +// Creates string | undefined +const Foo = optional(t.string); +``` + +## flattened + +Define codecs that flatten properties when decoding and nest them when encoding. + +### Example + +```typescript +const Item = flattened({ + first: { + second: t.string, + }, +}); + +// When decoded, you get: +type DecodedType = { + second: string; +}; + +// When encoded, you get: +type EncodedType = { + first: { + second: string; + }; +}; +``` + +You can flatten multiple top-level properties into one object: + +```typescript +const Item = flattened({ + foo: { + fizz: t.string, + }, + bar: { + buzz: t.number, + }, +}); + +// When decoded, you get: +type DecodedType = { + fizz: string; + buzz: number; +}; + +// When encoded, you get: +type EncodedType = { + foo: { + fizz: string; + }; + bar: { + buzz: number; + }; +}; +``` + +The library tries to prevent you from defining multiple nested properties with the same +key, but it can't catch all cases. If you work around this protection, the behavior is +undefined. diff --git a/website/docs/reference/io-ts-http/http-request.md b/website/docs/reference/io-ts-http/http-request.md new file mode 100644 index 00000000..ec15e5c8 --- /dev/null +++ b/website/docs/reference/io-ts-http/http-request.md @@ -0,0 +1,178 @@ +--- +sidebar_position: 3 +--- + +# httpRequest + +Define HTTP request codecs that create a flattened type from query parameters, path +parameters, headers, and body content. Pass an object with codecs for these components +to create a codec that flattens them when decoded and places them in their appropriate +positions when encoded. All parameters are optional and default to an empty object `{}`. + +## Properties + +You can include these optional properties in the object you pass to `httpRequest`: + +### params + +Define the types of path parameters in a URL. Include any URL parameters that appear in +the `path` property of your `httpRoute`. + +```typescript +export const GetMessage = httpRoute({ + path: '/message/{id}', + method: 'GET', + request: httpRequest({ + params: { + id: t.string, + }, + }), + // ... +}); +``` + +### query + +Define the query parameters that appear at the end of a URL. + +```typescript +export const GetMessages = httpRoute({ + path: '/messages', + method: 'GET', + request: httpRequest({ + query: { + author: t.string, + }, + }), + // ... +}); +``` + +### headers + +Define the [HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) +that should be included with the request. + +```typescript +export const GetSecureResource = httpRoute({ + path: '/secure-resource', + method: 'GET', + request: httpRequest({ + headers: { + authorization: t.string, + }, + }), + // ... +}); +``` + +### body + +Define the [HTTP body](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#body) +of the request, typically as a JSON object. + +```typescript +export const CreateMessage = httpRoute({ + path: '/message', + method: 'POST', + request: httpRequest({ + body: { + message: t.string, + }, + }), + // ... +}); +``` + +## Examples + +### Request with a single query parameter + +```typescript +const Request = httpRequest({ + query: { + message: t.string, + }, +}); + +// Decoded type +type DecodedRequest = { + message: string; +}; +``` + +### Request with both query and path parameters + +```typescript +const Request = httpRequest({ + query: { + message: t.string, + }, + params: { + id: NumberFromString, + }, +}); + +// Decoded type +type DecodedRequest = { + message: string; + id: number; +}; +``` + +### Request with a body + +```typescript +const Request = httpRequest({ + params: { + id: NumberFromString, + }, + body: { + content: t.string, + timestamp: DateFromISOString, + }, +}); + +// Decoded type +type DecodedRequest = { + id: number; + content: string; + timestamp: Date; +}; +``` + +## How flattening works + +When you decode `httpRequests` using `io-ts` helpers, the system flattens all properties +into a single object: + +```typescript +import { DateFromISOString, NumberFromString } from 'io-ts-types'; + +// Create a request with a path parameter and a body +const Request = httpRequest({ + params: { + id: NumberFromString, + }, + body: { + content: t.string, + timestamp: DateFromISOString, + }, +}); + +// Get the TypeScript type of the Request +type Request = t.TypeOf; + +// The resulting type is: +type Request = { + id: number; + content: string; + timestamp: Date; +}; +``` + +## Limitations + +Currently, `httpRequest` only supports object-type request bodies. For other body types, +see the workaround in the +[httpRoute documentation](./http-route#using-a-non-object-body-type). diff --git a/website/docs/reference/io-ts-http/http-route.md b/website/docs/reference/io-ts-http/http-route.md new file mode 100644 index 00000000..84839e50 --- /dev/null +++ b/website/docs/reference/io-ts-http/http-route.md @@ -0,0 +1,198 @@ +--- +sidebar_position: 2 +--- + +# httpRoute + +Define an individual HTTP route by providing an object with four properties. + +## Properties + +### path + +Specify the route's path with any path variables. Surround variables with brackets like +`{name}`. The request codec passes these variables to the `params` object. + +Example route with no path parameters: + +```typescript +httpRoute({ + path: '/example', + method: 'GET', + request: httpRequest({ + params: { + // Nothing needed here, `params` can be undefined + }, + }), + response: { + 200: t.string, + }, +}); +``` + +Example with parameters: + +```typescript +httpRoute({ + path: '/example/{id}', + method: 'GET', + request: httpRequest({ + params: { + id: NumberFromString, + }, + }), + response: { + 200: t.string, + }, +}); +``` + +### method + +Specify the HTTP method for this route. Use GET, POST, PUT, PATCH, or DELETE. We may add +support for other methods in the future. + +### request + +Define the codec that decodes incoming HTTP requests on the server side and encodes +outgoing requests on the client side. Typically, combine it with the `httpRequest` codec +builder function. + +```typescript +const Route = httpRoute({ + path: '/example/{id}', + method: 'GET', + request: httpRequest({ + params: { + id: NumberFromString, + }, + }), + response: { + 200: t.string, + }, +}); + +// Due to property flattening, the decoded type is: +type RequestProps = { + id: number; +}; + +// Create a client using `superagent-wrapper` +const routeApiClient: (props: RequestProps) => Promise; + +// The client handles type checking and parameter placement automatically +const response: string = await routeApiClient({ id: 1337 }); +``` + +### response + +Define the possible responses that a route may return, along with the codec for each +response. Response keys correspond to HTTP status codes. The system assumes incoming +responses are JSON. + +```typescript +const Route = httpRoute({ + path: '/example', + method: 'GET', + request: httpRequest({}), + response: { + 200: t.type({ + foo: t.string, + }), + 404: t.type({ + message: t.string, + }), + 400: t.type({ + message: t.string, + }), + }, +}); +``` + +## Advanced usage + +Sometimes the `httpRequest` function isn't expressive enough for a particular route. You +can combine it with other `io-ts` functions to work around limitations. The `request` +property passed to `httpRoute` only needs to conform to this output type: + +```typescript +type GenericHttpRequest = { + params: { + [K: string]: string; + }; + query: { + [K: string]: string | string[]; + }; + headers: { + [K: string]: string; + }; + body?: Json; +}; +``` + +Any codec that outputs an object matching that type works. Here are some common +workarounds: + +### Using a non-object body type + +By default, `httpRequest` assumes a request body is an object with properties that can +be flattened. For other types (like a string body), combine `httpRequest` with another +codec: + +```typescript +const StringBodyRoute = httpRoute({ + path: '/example/{id}', + method: 'POST', + request: t.intersection([ + httpRequest({ + params: { id: t.string }, + }), + t.type({ + body: t.string, + }), + ]), + response: { + 200: t.string, + }, +}); + +// Decoded type: +type DecodedType = { + id: string; + body: string; +}; +``` + +This approach adds a `body` property to the decoded type, which breaks the abstraction +but works effectively. + +### Creating conditional query parameters + +For query parameters that depend on other parameter values, use a union of multiple +`httpRequests`: + +```typescript +const UnionRoute = httpRoute({ + path: '/example', + method: 'GET', + request: t.union([ + httpRequest({ + query: { + type: t.literal('ping'), + }, + }), + httpRequest({ + query: { + type: t.literal('message'), + message: t.string, + }, + }), + ]), + response: { + 200: t.string, + }, +}); + +// Decoded type: +type DecodedType = { type: 'ping' } | { type: 'message'; message: string }; +``` diff --git a/website/docs/reference/io-ts-http/index.md b/website/docs/reference/io-ts-http/index.md new file mode 100644 index 00000000..d840d4b5 --- /dev/null +++ b/website/docs/reference/io-ts-http/index.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 1 +--- + +# io-ts-http Reference + +Find detailed technical reference documentation for the `io-ts-http` package in this +section. + +## Core components + +- [apiSpec](./api-spec): Define a collection of routes +- [httpRoute](./http-route): Create individual HTTP routes +- [httpRequest](./http-request): Build HTTP request codecs +- [combinators](./combinators): Create more expressive types + +## Installation + +```bash +npm install @api-ts/io-ts-http io-ts +``` + +## Basic usage + +```typescript +import * as t from 'io-ts'; +import { apiSpec, httpRoute, httpRequest } from '@api-ts/io-ts-http'; + +// Create a route +const GetUser = httpRoute({ + path: '/users/{id}', + method: 'GET', + request: httpRequest({ + params: { + id: t.string, + }, + }), + response: { + 200: t.type({ + id: t.string, + name: t.string, + email: t.string, + }), + 404: t.type({ + error: t.string, + }), + }, +}); + +// Define your API specification +export const API = apiSpec({ + 'api.v1.users': { + get: GetUser, + }, +}); +``` From f3dce24c05200c2ace17c292ee1b131b92d2c1a6 Mon Sep 17 00:00:00 2001 From: Young Jun Joo Date: Fri, 11 Apr 2025 09:26:06 -0400 Subject: [PATCH 2/3] chore: delete docs --- .../docs/explanation/io-ts-http-concepts.md | 112 ------- .../how-to-guides/advanced-http-routes.md | 234 --------------- .../how-to-guides/working-with-combinators.md | 280 ------------------ 3 files changed, 626 deletions(-) delete mode 100644 website/docs/explanation/io-ts-http-concepts.md delete mode 100644 website/docs/how-to-guides/advanced-http-routes.md delete mode 100644 website/docs/how-to-guides/working-with-combinators.md diff --git a/website/docs/explanation/io-ts-http-concepts.md b/website/docs/explanation/io-ts-http-concepts.md deleted file mode 100644 index 2c3d68da..00000000 --- a/website/docs/explanation/io-ts-http-concepts.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Understanding io-ts-http - -## Introduction - -Use `io-ts-http` as the definition language for api-ts specifications. It helps you -define precise API contracts for web servers. This article explains the key concepts and -design principles behind `io-ts-http`. - -## Core concepts - -### API contract as a type system - -Express your API contract through TypeScript's type system with runtime validation using -`io-ts`. This approach offers three main benefits: - -1. **Type safety**: Use the same types for both clients and servers to ensure - compatibility. -2. **Runtime validation**: Validate data at runtime, unlike TypeScript's types which are - erased during compilation. -3. **Parse, don't validate**: Not only validate input but also parse it into the correct - shape and types. - -### Building blocks - -`io-ts-http` has three main components: - -1. **`apiSpec`**: Create the top-level definition of your API and organize routes under - operation names. -2. **`httpRoute`**: Define individual routes with their paths, methods, request schemas, - and response schemas. -3. **`httpRequest`**: Specify the expected shape of HTTP requests, including parameters, - query strings, headers, and bodies. - -## How it works - -When you define an API with `io-ts-http`, you create a specification that works in -multiple ways: - -1. **Server-side**: Validate and parse incoming requests to ensure they match your API - contract. -2. **Client-side**: Ensure requests are properly formed before sending them to the - server. -3. **Documentation**: Generate API documentation such as OpenAPI specifications. - -The same specification works everywhere, eliminating discrepancies between your -implementation and documentation. - -### Property flattening - -Property flattening is an important concept in `io-ts-http`. When you decode HTTP -requests, the system flattens properties from different sources (path parameters, query -parameters, headers, and body) into a single object. - -For example, if you have: - -- A path parameter `id` -- A query parameter `filter` -- A body with properties `name` and `email` - -The decoded object becomes: - -```typescript -{ - id: string, - filter: string, - name: string, - email: string -} -``` - -This makes it easier to work with the data without accessing different parts of the -request separately. - -## Design principles - -### 1. Type safety first - -`io-ts-http` prioritizes type safety. It uses `io-ts` codecs to ensure your API -contracts are type-checked at compile time and validated at runtime. - -### 2. Separation of concerns - -The library separates: - -- Defining what your API looks like (`apiSpec` and `httpRoute`) -- Defining how requests and responses should be structured (`httpRequest`) -- Processing HTTP requests and responses (handled by other packages) - -### 3. Flexibility - -`io-ts-http` is flexible. It doesn't dictate how to implement your server or client—only -how to define your API contract. You can use it with any HTTP framework or client -library. - -## When to use io-ts-http - -Use `io-ts-http` when you need to: - -1. Build a TypeScript API with type safety between client and server -2. Validate complex request structures with nested objects, arrays, or optional fields -3. Generate documentation from your code to keep it current -4. Implement the "Parse, Don't Validate" pattern for robust error handling - -## Conclusion - -`io-ts-http` offers a powerful way to define type-safe API contracts for your entire -application stack. By centralizing your API definition in a single source of truth, you -can ensure consistency and reduce errors in your application. diff --git a/website/docs/how-to-guides/advanced-http-routes.md b/website/docs/how-to-guides/advanced-http-routes.md deleted file mode 100644 index 1c53da3b..00000000 --- a/website/docs/how-to-guides/advanced-http-routes.md +++ /dev/null @@ -1,234 +0,0 @@ -# Advanced HTTP route patterns - -Learn advanced patterns for defining HTTP routes with `io-ts-http` beyond the basic -usage examples. - -## Work with non-object body types - -By default, `httpRequest` assumes the request body is a JSON object. Sometimes you need -to accept other types like strings, numbers, or arrays. - -### Accept a string body - -Use `t.intersection` to combine `httpRequest` with a custom type that accepts a string -body: - -```typescript -import * as t from 'io-ts'; -import { httpRoute, httpRequest } from '@api-ts/io-ts-http'; - -const StringBodyRoute = httpRoute({ - path: '/example/{id}', - method: 'POST', - request: t.intersection([ - httpRequest({ - params: { id: t.string }, - }), - t.type({ - body: t.string, - }), - ]), - response: { - 200: t.string, - }, -}); - -// Decoded type -type DecodedType = { - id: string; - body: string; -}; -``` - -This approach breaks the abstraction slightly by exposing a `body` property in the -decoded type, but it works effectively. - -### Accept an array body - -Similarly, accept an array body: - -```typescript -const ArrayBodyRoute = httpRoute({ - path: '/batch-process', - method: 'POST', - request: t.intersection([ - httpRequest({}), - t.type({ - body: t.array(t.string), - }), - ]), - response: { - 200: t.type({ - processed: t.number, - }), - }, -}); - -// Decoded type -type DecodedType = { - body: string[]; -}; -``` - -## Create conditional request parameters - -Sometimes you need parameters that are required only when other parameters have specific -values. - -### Use union types - -Use `t.union` with multiple `httpRequest` objects to handle conditional parameters: - -```typescript -const SearchRoute = httpRoute({ - path: '/search', - method: 'GET', - request: t.union([ - // When searching by keyword - httpRequest({ - query: { - type: t.literal('keyword'), - keyword: t.string, - }, - }), - // When searching by category - httpRequest({ - query: { - type: t.literal('category'), - categoryId: NumberFromString, - }, - }), - // When searching by both - httpRequest({ - query: { - type: t.literal('combined'), - keyword: t.string, - categoryId: NumberFromString, - }, - }), - ]), - response: { - 200: t.array( - t.type({ - id: t.string, - title: t.string, - }), - ), - }, -}); - -// Decoded type will be a union: -type DecodedType = - | { type: 'keyword'; keyword: string } - | { type: 'category'; categoryId: number } - | { type: 'combined'; keyword: string; categoryId: number }; -``` - -## Add optional headers - -HTTP headers are often optional. Use the `optional` combinator to define optional -headers: - -```typescript -import { httpRequest, optional } from '@api-ts/io-ts-http'; - -const RequestWithOptionalHeaders = httpRoute({ - path: '/resource', - method: 'GET', - request: httpRequest({ - headers: { - authorization: t.string, - 'cache-control': optional(t.string), - 'if-modified-since': optional(t.string), - }, - }), - response: { - 200: t.object, - }, -}); - -// Decoded type -type DecodedType = { - authorization: string; - 'cache-control'?: string; - 'if-modified-since'?: string; -}; -``` - -## Handle file uploads - -File uploads typically use `multipart/form-data` encoding. While `io-ts-http` doesn't -directly support file uploads, you can treat the file as an opaque object in the type -system and handle the file processing separately: - -```typescript -const FileUploadRoute = httpRoute({ - path: '/upload', - method: 'POST', - request: httpRequest({ - body: { - // In the type system, just indicate a file is expected - // Your server framework will handle the actual file - file: t.unknown, - description: optional(t.string), - }, - }), - response: { - 200: t.type({ - fileId: t.string, - size: t.number, - }), - }, -}); -``` - -## Combine multiple request sources - -Sometimes you need to extract information from multiple sources, such as getting an ID -from the path, authentication from headers, and data from the body: - -```typescript -const ComplexRoute = httpRoute({ - path: '/users/{userId}/profile', - method: 'PUT', - request: httpRequest({ - params: { - userId: NumberFromString, - }, - headers: { - authorization: t.string, - }, - body: { - name: t.string, - email: t.string, - preferences: t.type({ - theme: t.union([t.literal('light'), t.literal('dark')]), - notifications: t.boolean, - }), - }, - }), - response: { - 200: t.type({ - success: t.boolean, - }), - }, -}); - -// Decoded type -type DecodedType = { - userId: number; - authorization: string; - name: string; - email: string; - preferences: { - theme: 'light' | 'dark'; - notifications: boolean; - }; -}; -``` - -## Summary - -These advanced patterns help you define complex HTTP routes that accurately reflect your -API's requirements. By combining `io-ts` with `httpRequest` and `httpRoute`, you can -create type-safe APIs with sophisticated validation logic. diff --git a/website/docs/how-to-guides/working-with-combinators.md b/website/docs/how-to-guides/working-with-combinators.md deleted file mode 100644 index c7407093..00000000 --- a/website/docs/how-to-guides/working-with-combinators.md +++ /dev/null @@ -1,280 +0,0 @@ -# Work with io-ts-http combinators - -Learn how to use combinators in `io-ts-http` to create more expressive and maintainable -API specifications. - -## Use the optional combinator - -Make specific properties optional in your request or response types with the `optional` -combinator. - -### Basic usage - -```typescript -import * as t from 'io-ts'; -import { httpRequest, optional } from '@api-ts/io-ts-http'; - -const UserSearchRequest = httpRequest({ - query: { - name: t.string, - age: optional(t.number), - city: optional(t.string), - }, -}); - -// Accepts requests with or without age and city parameters -// Decoded type: -// { -// name: string; -// age?: number; -// city?: string; -// } -``` - -### With nested objects - -```typescript -const ProductRequest = httpRequest({ - body: { - name: t.string, - price: t.number, - details: optional( - t.type({ - description: t.string, - dimensions: t.type({ - width: t.number, - height: t.number, - depth: t.number, - }), - }), - ), - }, -}); - -// Includes an optional details object in the decoded type -``` - -## Use the optionalize combinator - -Define object types with both required and optional properties easily using the -`optionalize` combinator. - -### Basic usage - -```typescript -import { optionalize } from '@api-ts/io-ts-http'; - -const UserProfile = optionalize({ - id: t.string, - name: t.string, - email: t.string, - phone: t.union([t.string, t.undefined]), // Optional - address: t.union([ - t.type({ - street: t.string, - city: t.string, - }), - t.undefined, - ]), // Optional -}); - -// Decoded type: -// { -// id: string; -// name: string; -// email: string; -// phone?: string; -// address?: { -// street: string; -// city: string; -// }; -// } -``` - -### Combine with optional - -Make your code more readable by using `optionalize` with `optional`: - -```typescript -const UserProfile = optionalize({ - id: t.string, - name: t.string, - email: t.string, - phone: optional(t.string), - address: optional( - t.type({ - street: t.string, - city: t.string, - }), - ), -}); - -// Same decoded type as above, but clearer intent -``` - -## Use the flattened combinator - -Decode nested objects into a flat structure, or encode flat objects into a nested -structure with the `flattened` combinator. - -### Basic usage - -```typescript -import { flattened } from '@api-ts/io-ts-http'; - -const NestedRequest = flattened({ - user: { - id: t.string, - name: t.string, - }, - metadata: { - createdAt: DateFromISOString, - updatedAt: DateFromISOString, - }, -}); - -// Input when encoding: -// { -// id: 'user123', -// name: 'John Doe', -// createdAt: new Date(), -// updatedAt: new Date(), -// } - -// Output after encoding: -// { -// user: { -// id: 'user123', -// name: 'John Doe', -// }, -// metadata: { -// createdAt: '2023-01-01T00:00:00.000Z', -// updatedAt: '2023-01-02T00:00:00.000Z', -// }, -// } -``` - -### With httpRequest - -Organize related properties in `httpRequest` using the `flattened` combinator: - -```typescript -const OrderRequest = httpRequest({ - params: { - orderId: t.string, - }, - body: flattened({ - customer: { - name: t.string, - email: t.string, - }, - shipping: { - address: t.string, - city: t.string, - zipCode: t.string, - }, - payment: { - method: t.union([t.literal('credit'), t.literal('debit'), t.literal('paypal')]), - amount: t.number, - }, - }), -}); - -// Decoded type: -// { -// orderId: string; -// name: string; -// email: string; -// address: string; -// city: string; -// zipCode: string; -// method: 'credit' | 'debit' | 'paypal'; -// amount: number; -// } -``` - -## Real-world examples - -### Create a user registration API - -```typescript -import * as t from 'io-ts'; -import { httpRoute, httpRequest, optional, optionalize } from '@api-ts/io-ts-http'; - -const RegisterUserRoute = httpRoute({ - path: '/users', - method: 'POST', - request: httpRequest({ - body: optionalize({ - username: t.string, - email: t.string, - password: t.string, - fullName: optional(t.string), - preferences: optional( - t.type({ - theme: t.union([t.literal('light'), t.literal('dark')]), - language: t.string, - }), - ), - }), - }), - response: { - 201: t.type({ - id: t.string, - username: t.string, - email: t.string, - }), - 400: t.type({ - error: t.string, - }), - }, -}); -``` - -### Create a search API with multiple parameters - -```typescript -const ProductSearchRoute = httpRoute({ - path: '/products/search', - method: 'GET', - request: httpRequest({ - query: optionalize({ - query: t.string, - category: optional(t.string), - minPrice: optional(NumberFromString), - maxPrice: optional(NumberFromString), - sort: optional( - t.union([ - t.literal('price_asc'), - t.literal('price_desc'), - t.literal('newest'), - t.literal('popular'), - ]), - ), - page: optional(NumberFromString), - limit: optional(NumberFromString), - }), - }), - response: { - 200: t.type({ - products: t.array( - t.type({ - id: t.string, - name: t.string, - price: t.number, - // Other product fields - }), - ), - total: t.number, - page: t.number, - pages: t.number, - }), - }, -}); -``` - -## Summary - -The combinators in `io-ts-http` help you define complex API specifications while keeping -your code readable and maintainable. Use these combinators to create type-safe API -contracts that accurately represent your application's requirements. From 7e1dc03f3e270e9d35e12dd8eba3d3d075148914 Mon Sep 17 00:00:00 2001 From: Young Jun Joo Date: Fri, 11 Apr 2025 09:27:20 -0400 Subject: [PATCH 3/3] chore: delete deleted contents --- packages/io-ts-http/README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/io-ts-http/README.md b/packages/io-ts-http/README.md index 9e5026c5..ab2c8919 100644 --- a/packages/io-ts-http/README.md +++ b/packages/io-ts-http/README.md @@ -63,15 +63,6 @@ For comprehensive documentation, visit our - [Create an HTTP server](https://bitgo.github.io/api-ts/docs/tutorial-basics/create-an-http-server) - [Create an HTTP client](https://bitgo.github.io/api-ts/docs/tutorial-basics/create-an-http-client) -- **How-to guides**: Problem-oriented guides for specific tasks - - - [Advanced HTTP routes](https://bitgo.github.io/api-ts/docs/how-to-guides/advanced-http-routes) - - [Working with combinators](https://bitgo.github.io/api-ts/docs/how-to-guides/working-with-combinators) - -- **Explanation**: Conceptual documentation - - - [Understanding io-ts-http](https://bitgo.github.io/api-ts/docs/explanation/io-ts-http-concepts) - - **Reference**: Technical documentation - [apiSpec](https://bitgo.github.io/api-ts/docs/reference/io-ts-http/api-spec) - [httpRoute](https://bitgo.github.io/api-ts/docs/reference/io-ts-http/http-route)