Extras

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