Fastify
Register contract handlers in a Fastify app.
Use @rest-rpc/fastify to implement and register contract routes in a Fastify
app.
The adapter registers routes by HTTP method and path, then delegates validation and response handling to the shared server layer.
Install
pnpm add @rest-rpc/core @rest-rpc/fastify fastify
Basic Setup
import { registerRoutes, router } from "@rest-rpc/fastify";
import Fastify from "fastify";
import { api } from "./contract";
const app = Fastify();
const routes = router(api, {
todos: {
get({ id, context }) {
const userAgent = context.req.headers["user-agent"];
void userAgent;
const todo = todos.get(id);
if (!todo) {
return {
status: 404,
body: {
code: "TODO_NOT_FOUND",
},
};
}
return todo;
},
},
});
registerRoutes(app, routes);
await app.listen({ port: 3000 });
Handler Input
Handlers receive flattened request input where context contains the Fastify
request object.
get({ id, "x-request-id": requestId, context }) {
return loadTodo({
id,
requestId,
req: context.req,
});
}
Handler Returns
Handlers can always return an explicit response envelope.
return {
status: 404,
body: {
code: "TODO_NOT_FOUND",
},
};
If a route has exactly one successful response, the handler can return the success body directly.
return todo;
You can also use ContractResponseError to throw a declared error response when
that’s more convenient.
throw new ContractResponseError(api.todos.get, {
status: 404,
body: {
code: "TODO_NOT_FOUND",
},
});
Error Handling
Use errorHandlers to customize server-generated error responses. Declared
responses returned from handlers, including ContractResponseError, do not run
through these hooks.
registerRoutes(app, routes, {
errorHandlers: {
onRequestValidationError: ({ issues }) => ({
status: 422,
body: { code: "VALIDATION_ERROR", issues },
}),
onUnhandledError: () => ({
status: 500,
body: { code: "INTERNAL_SERVER_ERROR" },
}),
},
});
Hooks
rest-rpc does not own middleware. Use Fastify hooks and plugin scopes the same
way you would in any Fastify app.
When hooks need to behave differently for different contract routes, use
createRouteMatcher to match the incoming request back to the contract route.
import { createRouteMatcher } from "@rest-rpc/fastify";
const matchContractRoute = createRouteMatcher(api);
app.addHook("preHandler", async (req, reply) => {
const match = matchContractRoute({
method: req.method,
path: req.url.split("?")[0] ?? req.url,
});
if (!match?.route.metadata?.requiresAuth) {
return;
}
const session = await loadSession(req.headers.authorization);
if (!session) {
return reply.status(401).send({ code: "UNAUTHORIZED" });
}
req.session = session;
});
Handlers can read values added by hooks through context.req.
const routes = router(api, {
todos: {
get({ id, context }) {
return loadTodoForSession({
id,
session: context.req.session,
});
},
},
});
Body Parsing
Fastify parses application/json and text/plain request bodies before the
route handler runs. The adapter reads req.body directly.
For custom request body content types, register a Fastify content type parser in the same scope as your routes.
app.addContentTypeParser(
"application/octet-stream",
{ parseAs: "buffer" },
(_req, body, done) => {
done(null, body);
},
);
Then declare the route with customBody(...); rest-rpc validates the parsed
body value against that schema.
WebSockets
Install @fastify/websocket when using WebSocket routes.
pnpm add @fastify/websocket
Fastify requires @fastify/websocket. Create the Fastify app, register the
plugin before registering routes, then pass WebSocket options to
registerRoutes.
beforeUpgrade runs after request validation and before the socket is accepted.
Return an upgrade rejection to deny the connection, or read and attach data to
context.req for the WebSocket handler.
import websocket from "@fastify/websocket";
import Fastify from "fastify";
import { registerRoutes } from "@rest-rpc/fastify";
const fastifyApp = Fastify();
await fastifyApp.register(websocket);
registerRoutes(fastifyApp, routes, {
webSocket: {
beforeUpgrade({ context, route, request }) {
context.req;
route;
request;
return undefined;
},
},
});
See WebSockets for the contract and handler model.