Common Options
All of the serverless handlers support a common set of options to configure the behavior of the handlers.
jsonQuery
- If set totrue
, the handler will automatically parse all query parameters as JSON values. Default isfalse
.responseValidation
- If set totrue
, the handler will validate the response body against the contract. Default isfalse
.basePath
- The base path for the handler. Default is/
. See Base Path for more information.cors
- Defaultfalse
- See CORS for more information.errorHandler
- See Custom Error Handler for more information.requestMiddleware
andresponseHandlers
- 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();
}),
],
},
});
};