Skip to main content

Common Options

All of the serverless handlers support a common set of options to configure the behavior of the handlers.

  • jsonQuery - If set to true, the handler will automatically parse all query parameters as JSON values. Default is false.
  • responseValidation - If set to true, the handler will validate the response body against the contract. Default is false.
  • basePath - The base path for the handler. Default is /. See Base Path for more information.
  • cors - Default false - See CORS for more information.
  • errorHandler - See Custom Error Handler for more information.
  • requestMiddleware and responseHandlers - See Middleware for more information.

Base Path

Set the basePath option if your handler's URL path does not start at the root of the domain.

For example, for a Vercel Function that lives inside api/posts.ts, you would set the basePath to /api/posts.

CORS

The cors option allows you to configure CORS for your handler. The cors option can be a boolean (setting it to false disables CORS) or an options object. We use itty-router under the hood for routing and CORS handling, so the options are the same as the itty-router CORS options. You can find the full list of options here.

Custom Error Handler

The errorHandler option allows you to define a custom error handler to handle any uncaught exceptions. The error handler is a function that takes an error and request and optionally returns a response object. If you do not return a response object, ts-rest will return a default 500 server error.

Note that this will catch

import { TsRestRequest, TsRestResponse } from '@ts-rest/serverless';

const errorHandler = (error: unknown, request: TsRestRequest) => {
console.error('Server Error', error);

return TsRestResponse.fromJson(
{ message: 'Custom Server Error Message' },
{ status: 500 }
);
};

Middleware

Global Request Middleware

You can set global request middleware by using the requestMiddleware option. These are functions that will be called sequentially before any router handler. This is useful for doing authentication, logging, etc.

You can optionally return a response object from the middleware to short-circuit the request.

You can also extend the request object with additional properties that can be accessed in the router handlers in a type-safe manner.

import { TsRestResponse } from '@ts-rest/serverless';
import { fetchRequestHandler, tsr } from '@ts-rest/serverless/fetch';
import { contract } from './contract';

export default async (request: Request) => {
return fetchRequestHandler({
request,
contract,
router: {
getPost: async ({ params: { id } }, { request }) => {
const post = prisma.post.findUniqueOrThrow({
where: { id, ownerId: request.userId },
});

return {
status: 200,
body: post,
};
},
},
options: {
requestMiddleware: [
tsr.middleware<{ userId: string }>((request) => {
if (request.headers.get('Authorization')) {
const userId = authenticate(request.headers.get('Authorization'));
if (!userId) {
return TsRestResponse.fromJson(
{ message: 'Unauthorized' },
{ status: 401 },
);
}
request.userId = userId;
}
}),
],
},
});
};

Global Response Handlers

You can set global response handlers by using the responseHandlers option. This can be useful to intercept all responses, including error responses, before they are sent. This can be useful for logging, adding headers, etc.

import { fetchRequestHandler, tsr } from '@ts-rest/serverless/fetch';
import { contract } from './contract';
import { router } from './router';

export default async (request: Request) => {
return fetchRequestHandler({
request,
contract,
router,
options: {
requestMiddleware: [
tsr.middleware<{ time: Date }>((request) => {
request.time = new Date();
}),
],
responseHandlers: [
(response, request) => {
console.log('Request took', new Date().getTime() - request.time.getTime(), 'ms');
},
],
},
});
}

Per Route Middleware

You can also set middleware for individual routes by passing an object in the form of { middleware: RequestHandler[], handler: (...) => ... } to the route definition.

import { fetchRequestHandler } from '@ts-rest/serverless/fetch';
import { contract } from './contract';

export default async (request: Request) => {
return fetchRequestHandler({
request,
contract,
router: {
getPost: {
middleware: [authenticationMiddleware],
handler: async ({ params: { id } }, { request }) => {
const post = prisma.post.findUniqueOrThrow({ where: { id, ownerId: request.userId } });

return {
status: 200,
body: post,
};
},
}
},
});
}

If you would like to have different request contexts defined in your global middleware and route-specific middleware, you can use the tsr.routeWithMiddleware() helper function. Please note, that you will need to manually pass the global request middleware context type, so it would be a good idea to define it outside your router definition and use that everywhere.

import { fetchRequestHandler, tsr } from '@ts-rest/serverless/fetch';
import { contract } from './contract';

type GlobalRequestContext = {
time: Date;
};

export default async (request: Request) => {
return fetchRequestHandler({
request,
contract,
router: {
getPost: tsr.routeWithMiddleware(contract.getPost)<
GlobalRequestContext, // <--- this is the global context
{ userId: string } // <--- this is the route-level context
>({
middleware: [
(request) => {
// do authentication
request.userId = '123';
},
],
handler: async ({ params: { id } }, { request }) => {
const post = prisma.post.findUniqueOrThrow({
where: { id, ownerId: request.userId },
});

return {
status: 200,
body: post,
};
},
}),
},
options: {
requestMiddleware: [
tsr.middleware<GlobalRequestContext>((request) => {
request.time = new Date();
}),
],
},
});
};