Universal REST API client for TypeScript.
✅ Pure REST
✅ Strongly typed
✅ Customizable
✅ Tested
✅ JSON (de)serialization out of the box
browser version:
npm i @tushinski/ts-rest
node.js version:
npm i @tushinski/ts-rest-node
import { getMapping, initClient } from 'ts-rest';
const restClient = {
users: {
get: getMapping<{}, { name: string }>(), // mapping for [GET] /users/:id
}
}
initClient({
client: restClient,
url: `https://example.com/rest`
})
restClient.users.get('alex')
.then(user => console.log(user.name));
// the resulting request: [GET] https://example.com/rest/alex
A plain object representing the tree structure of a target API.
For example, for an API, providing these methods:
[GET] /docs/:id
[POST] /docs/
[PUT] /docs/:id
a client will look like this:
const client = {
docs: {
get: getMapping<{}, Document>(),
post: postMapping<Document, Document>(),
put: putMapping<Document, Document>()
}
}
(where Document
is a user-defined type)
Ts-rest provides several functions for mappings.
Their generic types are used to specify types of different request parameters.
Mapping:
{
get: getMapping<ParamsType, ResponseType>()
}
Usage:
client.get(id?: string, params?: ParamsType)
Mapping:
{
getAll: getAllMapping<ParamsType, ResponseType>()
}
Usage:
client.getAll(params?: ParamsType)
Mapping:
{
post: postMapping<DataType, ResponseType>()
}
Usage:
client.post(body: DataType)
Mapping:
{
put: putMapping<DataType, ResponseType>()
}
Usage:
client.put(id: string, body: DataType)
Mapping:
{
delete: deleteMapping<DataType>()
}
Usage:
client.delete(id: string)
ResponseType
- type of response bodyDataType
- type of request bodyParamsType
- type of a search query parameters map
You can specify search query parameters using a plain object:
const moviesApiClient = {
movies: {
getAll: getAll<{genre: string, year: number}>()
}
}
// initialization...
moviesApiClient.movies.getAll({genre: 'drama', year: 1966})
In most of the cases paths of single resources end with a single path parameter - a resource id. But there are cases when a resource contains nested collections (or sub-resources), like:
<api_path>/actors/{actorId}/movies
Ts-rest provides special function sub
for describing sub resources:
const client = {
actors: {
single: sub(() => ({
movies: {
getAll: getAllMapping<{}, Movie[]>()
},
}))
}
}
client.actors.single(1).movies.getAll() // [GET] <api_path>/actors/1/movies
.then(movies => {/*...*/})
You can think of it like of getting a single resource by id:
<api_path>/actors/1
and working with it's sub-resources:
<api_path>/actors/1/movies
Since the single
method only returns a "sub-client" (and doesn't perform any requests),
it's result can be stored to a variable for reusing:
const actor1 = client.actors.single(1);
actor1.movies.getAll()
.then(movies => /*...*/);
actor1.awards.post(/* award data */)
.then(award => /*...*/);
For cases in which it is needed to declare a mapping for a varying sub-path,
there's a subPath
function:
const client = {
pdfDocument: subPath(/\.pdf$/, () => ({
get: getMapping<{}, Document>()
}))
}
client.pdfDocument("path/to/document/name.pdf").get() // [GET] <api_path>/path/to/document/name.pdf
.then(document => {/*...*/})
The first argument of subPath
is a regular expression which you can use to restrict sub-path.
If provided path doesn't match to the pattern, method will throw an error.
Request modifiers are used to modify request parameters and response data during a request.
They can be specified with the requestModifiers
option (see Initialization).
Modifies default request parameters (such as headers, content type, etc.);
optionsModifier: (defaultOptions: RequestModification, path: string, method: HTTPMethod) => RequestModification
Modifies request body.
bodyModifier: (resp: Response, path: string, method: HTTPMethod) => any
Modifies response data.
responseModifier: (body: any, path: string, method: HTTPMethod) => BodyInit | null
If custom modifiers are not specified, default modifiers are used:
{
optionsModifier: (defaultOptions) => defaultOptions,
bodyModifier: (body) => JSON.stringify(body),
responseModifier: (resp) => resp.json()
};