Skip to main content

Next.js Server

Installation​

pnpm add @ts-rest/next

Usage​

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

info

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.

Single Route Handler​

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);
return res.status(500).json({ message: 'Internal Server Error' });
}
},
});

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) {
return res
.status(400)
.json({ message: 'Malformed Body', errors: error.body.flatten() });
}

return res.status(400).json({ message: 'Bad Request' });
}
},
});
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');
}
}

Future Work​

As this pattern doesn't support a lambda per endpoint, it is planned to provide a helper utility to allow individual endpoints to be created.

The downside of this is that it will not be possible to give a Typescript warning if the path is incorrect.