Extras
on this page
Lets discuss a few more cool things we can do here.
Api versioning
API versioning is the practice of managing changes in a software application's programming interface (API) over time. It's crucial for maintaining compatibility and smooth transitions as APIs evolve. Versioning allows for consistent development, API stability, and client control, enabling gradual adoption of new features. It also supports legacy systems and provides clear documentation, as different API versions can be maintained concurrently. By implementing versioning strategies like version numbers in URLs, headers, or query parameters, developers ensure that changes can be made without disrupting existing functionality and integrations.
We can use path prefixes in a contract to achieve this behavior.
const contract = initContract();
export const contract = contract.router(
{
getPost: {
path: '/mypath',
//... Your Contract
},
},
{
pathPrefix: '/api/v1',
}
);
Now the endpoints of this contract will be /api/v1/<endpoint path>
.
Using Axios
The ts-rest client uses fetch
under the hood but it also provides you with the functionality to ovveride this by using your custom api handler such as axios. Taken right from the documentation of ts-rest is this really small example to integrate axios
into the ts-rest client.
import { contract } from './some-contract';
import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios';
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: response.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;
}
},
});
The ts-rest documentation is really good. You should really check it out.
Inferring Types
To get the response, and request types of a specific endpoint in an endpoint, we have the following type helpers:
import { ServerInferResponses, ClientInferResponseBody, ServerInferRequest } from '@ts-rest/core';
type LoginEndpointResponseType= ServerInferResponses<typeof UserContract.login>
type LoginEndpointResponseBodyType = ClientInferResponseBody<typeof UserContract.login>;
type LoginEndpointRequestType = ServerInferRequest<typeof UserContract.login>;
File uploads
To upload files, ts-rest lets you specify the contentType
as multipart/form-data
, and then you can upload files using the formData javascript API. This example is taken from the ts-rest documentation.
const c = initContract();
export const postsApi = c.router({
updatePostThumbnail: {
method: 'POST',
path: '/posts/:id/thumbnail',
contentType: 'multipart/form-data', // <- Only difference
body: c.type<{ thumbnail: File }>(), // <- Use File type in here
responses: {
200: z.object({
uploadedFile: z.object({
name: z.string(),
size: z.number(),
type: z.string(),
}),
}),
400: z.object({
message: z.string(),
}),
},
},
});