Callsheet
Guides

Generating Calls from ts-rest

Use Callsheet with ts-rest contracts, with or without code generation.

If you already have a ts-rest contract, you can generate Callsheet directly from it. Callsheet reads the contract and builds a shared call module your app can use anywhere.

Start from a contract

This guide assumes you already have a ts-rest contract. If you don't, ts-rest's own docs are the best place to get set up.

Here is a small contract with one query and one mutation:

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

const c = initContract();

export const contract = c.router({
  films: {
    byId: c.query({
      method: 'GET',
      path: '/films/:id',
      pathParams: c.type<{ id: string }>(),
      responses: {
        200: c.type<{ film: { id: string; title: string } }>(),
      },
    }),
    update: c.mutation({
      method: 'PATCH',
      path: '/films/:id',
      pathParams: c.type<{ id: string }>(),
      body: c.type<{ title: string }>(),
      responses: {
        200: c.type<{ updated: true }>(),
      },
    }),
  },
});

Point Callsheet at the contract

If you haven't already, install the packages you need for the generated ts-rest workflow:

pnpm add @callsheet/react-query @ts-rest/core
pnpm add -D @callsheet/codegen
npm install @callsheet/react-query @ts-rest/core
npm install -D @callsheet/codegen
yarn add @callsheet/react-query @ts-rest/core
yarn add @callsheet/codegen -D
pnpm add @callsheet/swr @ts-rest/core
pnpm add -D @callsheet/codegen
npm install @callsheet/swr @ts-rest/core
npm install -D @callsheet/codegen
yarn add @callsheet/swr @ts-rest/core
yarn add @callsheet/codegen -D

Then create a callsheet.config.ts at your project root:

callsheet.config.ts
import { defineConfig } from '@callsheet/codegen';

export default defineConfig({
  sources: {
    tsRest: [
      {
        importFrom: './src/rest/contract',
        exportName: 'contract',
        pathPrefix: ['rest'],
      },
    ],
  },
  output: {
    adapter: 'react-query',
    file: './src/generated/calls.ts',
  },
});
callsheet.config.ts
import { defineConfig } from '@callsheet/codegen';

export default defineConfig({
  sources: {
    tsRest: [
      {
        importFrom: './src/rest/contract',
        exportName: 'contract',
        pathPrefix: ['rest'],
      },
    ],
  },
  output: {
    adapter: 'swr',
    file: './src/generated/calls.ts',
  },
});

The tsRest source block tells Callsheet where to find your contract and how to read it:

  • importFrom is the path to the module that exports the contract.
  • exportName is the name of the contract export in that module.
  • pathPrefix is optional. It namespaces the generated calls under a prefix which is useful when your Callsheet also includes calls from other sources like GraphQL. In this example, the generated paths will start with rest.films.* instead of just films.*.

Generate your calls

Run codegen to build the call module:

pnpm callsheet-codegen --config callsheet.config.ts
npx callsheet-codegen --config callsheet.config.ts
yarn callsheet-codegen --config callsheet.config.ts

Callsheet reads the contract, finds each route, and infers whether it should be a query or mutation based on the route definition in your contract. The generated module looks like this:

src/generated/calls.ts
import { defineCalls, mutation, query } from '@callsheet/react-query';
import { contract } from '../rest/contract';

export const calls = defineCalls({
  rest: {
    films: {
      byId: query(contract.films.byId),
      update: mutation(contract.films.update),
    },
  },
});
src/generated/calls.ts
import { defineCalls, mutation, query } from '@callsheet/swr';
import { contract } from '../rest/contract';

export const calls = defineCalls({
  rest: {
    films: {
      byId: query(contract.films.byId),
      update: mutation(contract.films.update),
    },
  },
});

Each route in the contract becomes a call in the generated module. The contract is still the typed source, and the generated calls module is the shared surface your app imports from.

Use generated calls

Once generated, use the calls through your adapter:

import { queryOptions, useMutation, useQuery } from '@callsheet/react-query';
import { calls } from './generated/calls';

const film = useQuery(
  queryOptions(calls.rest.films.byId, {
    input: { params: { id: 'wall-e' } },
    select: (data) => data.film,
  }),
);

const updateFilm = useMutation(calls.rest.films.update);

To add shared defaults or invalidation rules, read Using Callsheet with React Query.

import { useQuery, useMutation } from '@callsheet/swr';
import { calls } from './generated/calls';

const { data: film } = useQuery(calls.rest.films.byId, {
  input: { params: { id: 'wall-e' } },
});

const { trigger: updateFilm } = useMutation(calls.rest.films.update);

To add shared defaults or invalidation rules, read Using Callsheet with SWR.

This is the same shared call surface used throughout the rest of the Callsheet docs. The only difference here is the typed source the calls were built from.

Without code generation

If you don't want to use codegen, you can define ts-rest calls manually using @callsheet/ts-rest. In that setup, add the adapter package your app uses, plus @callsheet/ts-rest, and wrap the routes yourself:

pnpm add @callsheet/react-query @callsheet/ts-rest
npm install @callsheet/react-query @callsheet/ts-rest
yarn add @callsheet/react-query @callsheet/ts-rest
pnpm add @callsheet/swr @callsheet/ts-rest
npm install @callsheet/swr @callsheet/ts-rest
yarn add @callsheet/swr @callsheet/ts-rest
src/calls.ts
import { defineCalls } from '@callsheet/react-query';
import { query, mutation } from '@callsheet/ts-rest';
import { contract } from './rest/contract';

export const calls = defineCalls({
  films: {
    byId: query(contract.films.byId),
    update: mutation(contract.films.update),
  },
});
src/calls.ts
import { defineCalls } from '@callsheet/swr';
import { query, mutation } from '@callsheet/ts-rest';
import { contract } from './rest/contract';

export const calls = defineCalls({
  films: {
    byId: query(contract.films.byId),
    update: mutation(contract.films.update),
  },
});

The result is the same shared call surface.

On this page