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

Express

Use rest-rpc with Express

Install

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

Usage

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

const app = express();
const router = express.Router();
const todos = new Map([["todo_1", { id: "todo_1", title: "Write docs" }]]);

router.use(express.json());

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 } };

registerRoutes(router, routes);
app.use("/api", router);
app.listen(3000);

registerRoutes() accepts an Express app or router. Mount a router to serve routes under a prefix.

Framework Context

Handlers and middleware receive the Express req and res objects in addition to the common handler arguments:

const getTodo = route
	.get("/todos/:id")
	.params(z.object({ id: z.string() }))
	.handler(({ params, req }) => {
		console.log(req.method, req.header("x-request-id"));
		return todos.get(params.id);
	});

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

Options

type RegisterRoutesOptions = {
	bodyCodecs?: readonly BodyCodec<Request>[];
	disableRequestValidation?: boolean;
	disableResponseValidation?: boolean;
	requestValidationErrorHandler?: RequestValidationErrorHandler;
	responseValidationErrorHandler?: ResponseValidationErrorHandler;
	middleware?: ExtendedExpressMiddleware[];
};

Middleware

Use the middleware option to run Express middleware only for rest-rpc routes. Each function has the Express middleware signature plus a route argument that contains the matched route declaration, including its metadata.

The middleware is registered on each rest-rpc route, so it does not run for other routes on the same app or router. It runs before rest-rpc decodes and validates the request, and before route middleware.

registerRoutes(app, routes, {
	middleware: [
		(req, res, next, route) => {
			if (route.metadata?.auth !== "required") return next();
			if (!req.header("authorization")) {
				res.status(401).send({ code: "UNAUTHORIZED" });
				return;
			}
			next();
		},
	],
});

Request and Response Bodies

Express parses request bodies. Register parsers such as express.json(), express.urlencoded(), or express.text() for the content types your routes accept; rest-rpc validates the resulting req.body. A custom codec’s deserialize receives the Express Request and replaces req.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 must be a string, Buffer, Uint8Array, or Node Readable.

Error Handling

Use requestValidationErrorHandler and responseValidationErrorHandler to replace the default validation error responses. Other errors are passed to the next Express error handler.

registerRoutes(app, routes, {
	requestValidationErrorHandler: (error, req, res) => {
		res.status(422).json({ code: "VALIDATION_ERROR", issues: error.issues });
	},
	responseValidationErrorHandler: (_error, req, res) => {
		res.status(500).json({ code: "INVALID_RESPONSE" });
	},
});

app.use((error, req, res, next) => {
	res.status(500).json({ code: "INTERNAL_SERVER_ERROR" });
});

Was this page helpful?