Zod 4 (and all other Standard Schema support) is available as a release candidate `3.53.0-rc.1`
ts-rest
Server

Next.js

How to use ts-rest with Next.js

Installation

pnpm add @ts-rest/next
bun add @ts-rest/next
npm install @ts-rest/next

Usage

For maximum type safety, by default @ts-rest/next uses a single endpoint for all routes.

Multiple Endpoints

It is possible to split out to multiple endpoints by having multiple [...ts-rest].tsx files in different folders, so long as the routers you use all begin with the same path e.g. all /posts endpoints in a posts folder.

// pages/api/[...ts-rest].tsx
import { createNextRoute, createNextRouter } from '@ts-rest/next';

// `contract` is the AppRouter returned by `c.router`
const postsRouter = createNextRoute(contract.posts, {
  createPost: async (args) => {
    const newPost = await posts.createPost(args.body);

    return {
      status: 201,
      body: newPost,
    };
  },
});

const router = createNextRoute(contract, {
  posts: postsRouter,
});

// Actually initiate the collective endpoints
export default createNextRouter(contract, router);

createNextRouter is a function that takes a contract and a complete router, and creates endpoints, with the correct methods, paths and callbacks.

It's also possible to create a handler for a single contract route.

// pages/api/posts/index.tsx
import { createSingleRouteHandler } from '@ts-rest/next';

export default createSingleRouteHandler(api.posts.createPost, async (args) => {
  const newPost = await posts.createPost(args.body);

  return {
    status: 201,
    body: newPost,
  };
});

createSingleRouteHandler is a function that takes a single contract route and a handler, and creates a nextApiHandler

JSON Query Parameters

To handle JSON query parameters, you can use the jsonQuery option.

export default createNextRouter(contract, router, { jsonQuery: true });

Response Validation

To enable response parsing and validation, you can use the validateResponses option. If there is a corresponding response Zod schema defined in the contract for the returned status code, the response will be parsed and validated. If validation fails a ResponseValidationError will be thrown causing a 500 response to be returned.

export default createNextRouter(contract, router, { validateResponses: true });

Error Handling

You can create a global error handler to handle any thrown errors by using the errorHandler option. This includes response validation errors.

export default createNextRouter(contract, router, {
  responseValidation: true,
  errorHandler: (error: unknown, req: NextApiRequest, res: NextApiResponse) => {
    if (error instanceof ResponseValidationError) {
      console.log(error.cause);
      res.status(500).json({ message: 'Internal Server Error' });
      return;
    }
  },
});

Request Validation Error Handling

The default behavior when validating request schema is to return a 400 status with the corresponding ZodError. To customize handling of the request validation errors enable the throwRequestValidation option. This option allows you to handle the request validation error in the global errorHandler function.

export default createNextRouter(contract, router, {
  throwRequestValidation: true,
  errorHandler: (error: unknown, req: NextApiRequest, res: NextApiResponse) => {
    if (error instanceof RequestValidationError) {
      if (error.body !== null) {
        res
          .status(400)
          .json({ message: 'Malformed Body', errors: error.body.flatten() });
        return;
      }

      res.status(400).json({ message: 'Bad Request' });
      return;
    }
  },
});
export class RequestValidationError extends Error {
  constructor(
    public pathParams: z.ZodError | null,
    public headers: z.ZodError | null,
    public query: z.ZodError | null,
    public body: z.ZodError | null,
  ) {
    super('[ts-rest] request validation failed');
  }
}