Fastify
Use rest-rpc with Fastify
Install
npm install @rest-rpc/fastifypnpm add @rest-rpc/fastifyyarn add @rest-rpc/fastifybun add @rest-rpc/fastifynub add @rest-rpc/fastifyaube add @rest-rpc/fastifyUsage
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[];
};
bodyCodecs: see Request and Response Bodies.disableRequestValidation,disableResponseValidation: see Disabling Server Validation.requestValidationErrorHandler,responseValidationErrorHandler: see Error Handling.preHandler: see preHandler.
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" });
});