Skip to main content

Custom Client

By default, not specifying an api will use the default tsRestFetchApi which uses fetch under the hood.

import { initClient } from '@ts-rest/core';
import { contract } from './contract';

const client = initClient(contract, {
baseUrl: 'http://localhost:5003',
baseHeaders: {},
// Uses `tsRestFetchApi` by default
});

Adding a Custom API

If you want a custom api, you can reuse the internal tsRestFetchApi to add logging/custom logic to your requests!

import { initClient, tsRestFetchApi } from '@ts-rest/core';
import { contract } from './contract';

const client = initClient(contract, {
baseUrl: 'http://localhost:5003',
baseHeaders: {},
api: async (args) => {
// Add anything you want here!

return tsRestFetchApi(args);
},
});

Extra Query Arguments

By default when you make a ts-rest request you can pass in params, query, body, headers etc. However, sometimes you may want to pass in extra arguments to your custom api. You can do this by extending the type of the args parameter in your custom api.

const client = initClient(contract, {
baseUrl: 'http://localhost:5003',
baseHeaders: {},
api: async (args: ApiFetcherArgs & { myCustomArg?: string }) => {
if (args.myCustomArg) {
// do something with myCustomArg ✨
}

return tsRestFetchApi(args);
},
});

The magical bit, is this is now fully typed and will work with your IDE's autocomplete! 🤯

One note here, any extra args which are provided here but aren't typed correctly - e.g. if you've @ts-expect-error'd, will still be passed to your api. This is because the args parameter is a spread of all the other arguments you pass in to your api.

client.getPosts({
query: { skip: 0, take: 10 },
myCustomArg: 'hello',
// ^-- autocomplete will work here, allowing you to extend ts-rest however you want
});
tip

You can use this to accomplish loads of patterns, such as adding a cache argument to your api, or adding a logger argument to your api - maybe you want to add an onUploadProgress argument to your api to track upload progress? You can do all of this with the args parameter!

Accessing the raw body and content type

Ts-Rest automatically stringifies your body input for mutations. You can access the raw body object and content-type like so:

const client = initClient(contract, {
baseUrl: 'http://localhost:5003',
baseHeaders: {},
api: async ({ path, method, headers, body, rawBody, contentType }) => {
// do something with rawBody ✨
return tsRestFetchApi(args);
},
});

Using Axios (custom api override)

By default ts-rest ships with an incredibly simple fetch implementation for data fetching, because fetch requires zero extra dependencies and works perfectly for most use cases, however, sometimes you may want to use Axios, or another data fetching strategy, for that you can pass an api attribute to the initClient or initQueryClient.

info

The credentials option has no effect when using a custom client. Make sure you handle credentials in your custom client (e.g., setting withCredentials in axios).

Here's a basic example:

import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios';
import { initClient } from '@ts-rest/core';
import { contract } from './contract';

const client = initClient(contract, {
baseUrl: 'http://localhost:3333/api',
baseHeaders: {
'Content-Type': 'application/json',
},
api: async ({ path, method, headers, body }) => {
const baseUrl = 'http://localhost:3333/api'; //baseUrl is not available as a param, yet
try {
const result = await axios.request({
method: method as Method,
url: `${this.baseUrl}/${path}`,
headers,
data: body,
});
return { status: result.status, body: result.data, headers: result.headers };
} catch (e: Error | AxiosError | any) {
if (isAxiosError(e)) {
const error = e as AxiosError;
const response = error.response as AxiosResponse;
return { status: response.status, body: response.data, headers: response.headers };
}
throw e;
}
},
});

Sometimes you need dynamic headers, IE passing in a Bearer token. There are two approaches you can take:

Instantiate the client with the header passed in:

import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios';
import { initClient } from '@ts-rest/core';
import { contract } from './contract';

export class SampleAPI {
token: string;
constructor(params: { token: string }) {
this.token = params.token;
this.baseUrl = 'http://localhost:3333/api';
}
client = () => {
return initClient(contract, {
baseUrl: this.baseUrl,
baseHeaders: {
Authorization: `Bearer ${idToken}`,
'Content-Type': 'application/json',
},
api: async ({ path, method, headers, body }) => {
try {
const result = await axios.request({
method: method as Method,
url: `${this.baseUrl}/${path}`,
headers,
data: body,
});
return { status: result.status, body: result.data, headers: result.headers };
} catch (e: Error | AxiosError | any) {
if (isAxiosError(e)) {
const error = e as AxiosError;
const response = error.response as AxiosResponse;
return { status: response.status, body: response.data, headers: response.headers };
}
throw e;
}
},
});
};
}

Instantiate the client but access a token during runtime:

Here's an example using the firebase/auth library. Because api is async, you can await various calls when using the method.

import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios';
import { initClient } from '@ts-rest/core';
import { contract } from './contract';

export class SampleAPI {
authInstance: Auth;
constructor(params: { authInstance: Auth }) {
this.authInstance = params.authInstance;
}
client = () => {
return initClient(contract, {
baseUrl: '',
baseHeaders: {
'Content-Type': 'application/json',
},
api: async ({ path, method, headers, body }) => {
const token = await this.authInstance.currentUser.getIdToken();
try {
const result = await axios.request({
method: method as Method,
url: `${this.baseUrl}/${path}`,
headers: {
...headers,
Authorization: `Bearer ${idToken}`,
},
data: body,
});
return { status: result.status, body: result.data, headers: result.headers };
} catch (e: Error | AxiosError | any) {
if (isAxiosError(e)) {
const error = e as AxiosError;
const response = error.response as AxiosResponse;
return { status: response.status, body: response.data, headers: result.headers };
}
throw e;
}
},
});
};
}

:::