Fetch Client
All of the client libraries (@ts-rest/core
, @ts-rest/react-query
, and @ts-rest/solid-query
) all use the initClient
or initQueryClient
functions to create a client. These functions take a baseUrl
and baseHeaders
as the first two arguments, and then an optional api
argument as the third argument.
import { initClient } from '@ts-rest/core';
import { getAccessToken } from '@some-auth-lib/sdk';
import { contract } from './contract';
export const client = initClient(contract, {
baseUrl: 'http://localhost:3334',
baseHeaders: {
'x-app-source': 'ts-rest',
'x-access-token': () => getAccessToken(),
},
});
To customise the fetching behaviour, add an api
function, please see Custom API for more information.
Query and Mutate
Your contract uses regular HTTP methods to define the type of request you want to make. We use this type to infer whether you are doing a query
or a mutate
request.
Any requests that use the GET
method will be treated as a query
request, and any requests that use the POST
, PUT
, PATCH
or DELETE
methods will be treated as a mutate
request.
const { data } = await client.posts.get();
const { data, status } = await client.posts.create({
body: {
title: 'My Post',
content: 'This is my post',
},
});
Breaking down the arguments:
body
- The body of the request, only used forPOST
,PUT
,PATCH
requests.query
- The query parameters of the request.headers
- Request headers defined in the contract (merged and overrides anybaseHeaders
defined in the client)extraHeaders
- If you want to pass headers not defined in the contractparams
- The path parameters of the request.fetchOptions
- Additional fetch options to pass to the fetch function.overrideClientOptions
- Override the client options for this request.
You can add your own custom arguments to the request, and they will be passed through to the api
function - Read more here! Custom API
const client = initClient(contract, {
// ...
api: async (args: ApiFetcherArgs & { custom?: string }) => {
return tsRestFetchApi(args);
},
});
const { data } = await client.getPosts({
custom: 'argument',
});
Understanding the return type
Because we type status codes, to check if the request was successful, we can use the status
property.
const data = await client.posts.create({
body: {
title: 'My Post',
content: 'This is my post',
},
});
if (data.status === 200) {
console.log('Success');
} else {
console.log('Something went wrong');
}
The data
property is typed as follows:
const data: {
status: 200;
body: User;
headers: Headers
} | {
status: 400 | 100 | 101 | 102 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 300 | 301 | 302 | 303 | 304 | 305 | 307 | ... 36 more ... | 511;
body: unknown;
headers: Headers
}
In this context, the term 'headers' refers to the response headers retrieved either from the default Fetch client or a custom client implementation.
Credentials (sending cookies)
The fetch()
function used by ts-rest does not send cookies in cross-origin requests by default unless the credentials
option is set to include
.
const client = initClient(contract, {
baseUrl: 'http://localhost:3333/api',
baseHeaders: {},
credentials: 'include',
});
Typed Query Parameters
By default, all query parameters are encoded as strings, however, you can use the jsonQuery
option to encode query parameters as typed JSON values.
Make sure to enable JSON query handling on the server as well.
const client = initClient(contract, {
baseUrl: 'http://localhost:3334',
baseHeaders: {},
jsonQuery: true,
});
const { data } = await client.posts.get({
query: {
take: 10,
skip: 0,
search: 'hello',
},
});
Objects implementing .toJSON()
will irreversibly be converted to JSON, so you will need to use custom zod transforms to convert back to the original object types.
For example, Date objects will be converted ISO strings by default, so you could handle this case like so:
const dateSchema = z
.union([z.string().datetime(), z.date()])
.transform((date) => (typeof date === 'string' ? new Date(date) : date));
This will ensure that you could pass Date objects in your client queries. They will be converted to ISO strings in the JSON-encoded URL query string, and then converted back to Date objects on the server by zod's parser.
Validate Response
The validateResponse
option allows you to validate the response body against the response schema. This is useful for ensuring that the server is returning the correct response type or performing transformations that are part of the response schema. For this to work, the responses schema must be defined using Zod (c.type<>
will not check types at runtime).
const c = initContract();
export const contract = c.router({
method: 'GET',
path: '/post/:id',
responses: {
200: z.object({
id: z.string(),
createdAt: z.coerce.date(),
}),
},
});
const client = initClient(contract, { validateResponse: true });
const response = await client.getPost({ id: '1' });
// response will be validated against the response schema
if (response.status === 200) {
// response.data will be of type { id: string, createdAt: Date }
// because `createdAt` has transformation of `z.coerce.date()`, it will parse any string date into a Date object
}
If you are doing any non-idempotent Zod transforms that run on the server, response validation may fail on the client or produce an unintended double transformation is certain cases. Make sure your transformations are idempotent.
// ❌❌
z.date().transform((d) => d.toISOString());
z.number().transform((n) => n + 1000);
z.string().transform((s) => `Hello, ${s}`);
// ✅✅
z.coerce.date();
z.string().transform((s) => s.toUpperCase());
z.union([z.string().datetime(), z.date()])
.transform((date) => (typeof date === 'string' ? new Date(date) : date));
Notes About Basic Fetch Client
We use the fetch API under the hood.
Our built-in fetch client handles the majority of use cases such as automatically parsing response bodies to JSON or text based on the Content-Type
header.
However, if you need to handle some different behavior, or add extra functionality such as injecting API tokens into requests, you can implement your own
custom fetcher or wrap the built-in fetcher.