Types for (de)serializing HTTP requests from both the client and server side
Runtime types for (de)serializing HTTP requests from both the client and server side
- @api-ts/io-ts-http
- Contents
- Preface
- Introduction
- Overview
- Example
- apiSpec
- httpRoute
- path
- method
- request
- response
- httpRequest
- params
- query
- headers
- body
- Decoding an httpRequest
- Documentation
This package extends io-ts with functionality useful
for typing HTTP requests. Start there for base knowledge required to use this package.
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.
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 queryhttpRequest
string parameters (basically just the results of splitting up the query string and
urlDecoding the values). The function can be combined with codecs fromio-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, andDate
if it is present, validate and parse it to a . If decoding succeeds, then the
resulting value's type will be:
`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';
const { id, time } = pipe(
ExampleHttpRequest.decode(request),
E.getOrElseW((decodeErrors) => {
someErrorHandler(decodeErrors);
}),
);
`
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.
Let's define the api-ts spec for a hypothetical message-user service. The conventionalapiSpec
top-level export is an
value; for example:
`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 httpRoutes ({Get|Create}Message,{Get|Create|Update|Delete}User
and ) which we'll discuss below.
> Currently, if you add the @version JSDoc tag to the exported API spec, it will beversion
> used as the API 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 apiSpecapi/v1/message
with two endpoints and api/v1/user. The api/v1/message endpointGET
responds to and POST verbs while the second reponds to GET, POST, PUT, andDELETE verbs using httpRoutes defined in ./routes/message. The following are thehttpRoutes defined in ./routes/message.
`typescript
import * as t from 'io-ts';
import { httpRoute, httpRequest } from '@api-ts/io-ts-http';
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,
}),
},
});
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 documentation for more context on how to use it
and this package.
Then httpRoute and httpRequest are imported. We'll review thehttpRequest below, but first, let's review the GetMessagehttpRoute.
`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,
}),
},
});
httpRoutes
apiSpecs usehttpRoutespath
to define the , method, request and response of a route.
#### path
The route's path along with possible path variables. You should surround variables{name}
with brackets like , and are to the request codec under the params property.
#### method
The route's method is theGetMessage
HTTP request method to use
for that route. In our example, the method is GET, while in ourPostMessage 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 possibleresponse
HTTP responses the route can
produce. The key-value pairs of the object are an HTTP status code followedio-ts
by the type of the response body. In our GetMessage example, a 200 statusmessage
response yields a payload of a JSON object with two properties, which is astring and id which is also a string, and a 404 yeilds a payload of a JSONerror
object with a single property which is a String.
Use httpRequest to build the expected type that you pass in a request to the route. InGetMessage
our example
`typescript`
export const GetMessage = httpRoute({
path: '/message/{id}',
method: 'GET',
request: httpRequest({
params: {
id: t.string,
},
}),
// ...
});
httpRequests 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 URLpath
parameters in the property of an httpRoute must be accounted for in theparams property of the httpRequest. Our request has a single URL parameter it isid
expecting, . This is the type of this parameter is captured in the params objecthttpRequest
of our .
#### query
query is the object representing the values passed in via query parameters at the endGetMessages
of a URL. The following example uses a new route, , to our API thatauthor
searches messages related to a specific :
`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 passed in with
a request.
#### body
body is an object representing the type of theCreateMessage
HTTP body of a
request. Often this is a JSON object. The httpRoute in our examplebody
uses the property:
`typescript`
export const CreateMessage = httpRoute({
path: '/message',
method: 'POST',
request: httpRequest({
body: {
message: t.string,
},
}),
// ...
});
#### Decoding an httpRequest
When you decode httpRequests using io-ts helpers, the properties of the request are
flattened like this:
`typescript
import { DateFromISOString, NumberFromString } from 'io-ts-types';
// 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,
},
});
// use io-ts to get the type of the Request
type Request = t.TypeOf
// same as
type Request = {
id: number;
content: string;
timestamp: Date;
};
``