Skip to content
rest-rpc
Esc
↑↓navigate↵open⌘Jpreview
On this page

Fastify

Use rest-rpc with Fastify

Install

npm install @rest-rpc/fastify
pnpm add @rest-rpc/fastify
yarn add @rest-rpc/fastify
bun add @rest-rpc/fastify
nub add @rest-rpc/fastify
aube add @rest-rpc/fastify

Usage

import Fastify from "fastify";
import { route, registerRoutes } from "@rest-rpc/fastify";
import { z } from "zod";

const app = Fastify({ logger: true });
const todos = new Map([["todo_1", { id: "todo_1", title: "Write docs" }]]);

const getTodo = route
	.get("/todos/:id")
	.params(z.object({ id: z.string() }))
	.handler(({ params }) => {
		const todo = todos.get(params.id);
		return todo
			? { status: 200, body: todo }
			: { status: 404, body: { code: "TODO_NOT_FOUND" as const } };
	});

export const routes = { todos: { get: getTodo } };

await app.register(
	async (api) => {
		registerRoutes(api, routes);
	},
	{ prefix: "/api" },
);

await app.listen({ port: 3000 });

registerRoutes() accepts a Fastify instance. Register it inside a plugin to serve routes under a prefix.

Framework Context

Handlers and middleware receive the Fastify req and reply objects in addition to the common handler arguments:

const getTodo = route
	.get("/todos/:id")
	.params(z.object({ id: z.string() }))
	.handler(({ params, req }) => {
		req.log.info({ todoId: params.id }, "Reading todo");
		return todos.get(params.id);
	});

To share an application context type across routes, augment DefaultContext in @rest-rpc/fastify.

Options

type RegisterRoutesOptions = {
	bodyCodecs?: readonly BodyCodec<FastifyRequest>[];
	disableRequestValidation?: boolean;
	disableResponseValidation?: boolean;
	requestValidationErrorHandler?: RequestValidationErrorHandler;
	responseValidationErrorHandler?: ResponseValidationErrorHandler;
	preHandler?: ExtendedFastifyPreHandler[];
};

preHandler

Use the preHandler option to run Fastify preHandler hooks only for rest-rpc routes. Each hook receives req and reply plus a route argument that contains the matched route declaration, including its metadata.

The hooks are registered on each rest-rpc route, so they do not run for other routes in the same Fastify instance. They run before rest-rpc decodes and validates the request, and before route middleware.

registerRoutes(app, routes, {
	preHandler: [
		async (req, reply, route) => {
			if (route.metadata?.auth !== "required") return;
			if (!req.headers.authorization) {
				return reply.status(401).send({ code: "UNAUTHORIZED" });
			}
		},
	],
});

Request and Response Bodies

Fastify parses request bodies with its content-type parsers. Add parsers with addContentTypeParser() for content types beyond Fastify’s defaults; rest-rpc validates the resulting request.body. A custom codec’s deserialize receives the FastifyRequest and replaces request.body for its content types.

Handlers can return these values for the default codecs:

Content type Value
application/json or any +json media type JSON value
application/x-www-form-urlencoded URLSearchParams
text/* string
Any other media type Blob, Uint8Array (including Buffer), or Node Readable

multipart/form-data responses need a custom serializer. A custom serializer’s body is passed to reply.send(). Return encoded bytes when the codec must control the exact payload, because Fastify serializes plain objects itself.

Error Handling

Use requestValidationErrorHandler and responseValidationErrorHandler to replace the default validation error responses. Other errors use Fastify’s error handler.

registerRoutes(app, routes, {
	requestValidationErrorHandler: (error, request, reply) => {
		return reply.status(422).send({
			code: "VALIDATION_ERROR",
			issues: error.issues,
		});
	},
	responseValidationErrorHandler: (_error, request, reply) => {
		return reply.status(500).send({ code: "INVALID_RESPONSE" });
	},
});

app.setErrorHandler((error, request, reply) => {
	return reply.status(500).send({ code: "INTERNAL_SERVER_ERROR" });
});

Was this page helpful?