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
});
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
.
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;
}
},
});
};
}
:::