Skip to content

tushinski/ts-rest

Repository files navigation

ts-rest

Universal REST API client for TypeScript.

✅ Pure REST
✅ Strongly typed
✅ Customizable
✅ Tested
✅ JSON (de)serialization out of the box

Installation

browser version:
npm i @tushinski/ts-rest

node.js version:
npm i @tushinski/ts-rest-node

Quick start

☝ Create a client

    import { getMapping, initClient } from 'ts-rest';

    const restClient = {
        users: {
            get: getMapping<{}, { name: string }>(), // mapping for [GET] /users/:id
        }
    }

✌ Initialize

    initClient({
        client: restClient,
        url: `https://example.com/rest`
    })

🤟 Use

    restClient.users.get('alex')
        .then(user => console.log(user.name));

    // the resulting request: [GET] https://example.com/rest/alex

Documentation

Client

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)

Mappings

Ts-rest provides several functions for mappings.
Their generic types are used to specify types of different request parameters.

GET (single resource)

Mapping:

{
    get: getMapping<ParamsType, ResponseType>()
}

Usage:

client.get(id?: string, params?: ParamsType)

GET (collection)

Mapping:

{
    getAll: getAllMapping<ParamsType, ResponseType>()
}

Usage:

client.getAll(params?: ParamsType)

POST

Mapping:

{
    post: postMapping<DataType, ResponseType>()
}

Usage:

client.post(body: DataType)

PUT

Mapping:

{
    put: putMapping<DataType, ResponseType>()
}

Usage:

client.put(id: string, body: DataType)

DELETE

Mapping:

{
    delete: deleteMapping<DataType>()
}

Usage:

client.delete(id: string)

Mapping types

  • ResponseType - type of response body
  • DataType - type of request body
  • ParamsType - type of a search query parameters map

Search query parameters (ParamsType)

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})

Sub-resources

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 => /*...*/);

Sub-paths

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

Request modifiers are used to modify request parameters and response data during a request.

They can be specified with the requestModifiers option (see Initialization).

Options modifier

Modifies default request parameters (such as headers, content type, etc.);

optionsModifier: (defaultOptions: RequestModification, path: string, method: HTTPMethod) => RequestModification

Body modifier

Modifies request body.

bodyModifier: (resp: Response, path: string, method: HTTPMethod) => any

Response modifier

Modifies response data.

responseModifier: (body: any, path: string, method: HTTPMethod) => BodyInit | null

Default modifiers

If custom modifiers are not specified, default modifiers are used:

{
    optionsModifier: (defaultOptions) => defaultOptions,
    bodyModifier: (body) => JSON.stringify(body),
    responseModifier: (resp) => resp.json()
};