Express
Use rest-rpc with Express
Install
npm install @rest-rpc/expresspnpm add @rest-rpc/expressyarn add @rest-rpc/expressbun add @rest-rpc/expressnub add @rest-rpc/expressaube add @rest-rpc/expressUsage
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[];
};
bodyCodecs: see Request and Response Bodies.disableRequestValidation,disableResponseValidation: see Disabling Server Validation.requestValidationErrorHandler,responseValidationErrorHandler: see Error Handling.middleware: see Middleware.
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" });
});