Node HTTP
Use rest-rpc with Node.js HTTP API
Install
pnpm add @rest-rpc/node
Usage
Create a handler
import { createRouteHandler, implement } from "@rest-rpc/node";
import { api } from "./contract";
const routes = {
todos: {
get: implement(api).todos.get.handler(({ id }) => getTodo(id)),
},
};
const handle = createRouteHandler(routes);import { createServer } from "node:http";
import { createRouteHandler, route } from "@rest-rpc/node";
const routes = {
health: route.get("/health").handler(() => ({ status: 204 })),
};
const handle = createRouteHandler(routes);Integrate with a framework
import { createServer } from "node:http";
import { createRouteHandler } from "@rest-rpc/node";
import { routes } from "./routes";
const handle = createRouteHandler(routes);
const server = createServer(async (req, res) => {
const { matched } = await handle(req, res);
if (matched) {
return;
}
res.statusCode = 404;
res.end("Not found");
});
server.listen(3000);import express from "express";
import { createRouteHandler } from "@rest-rpc/node";
import { routes } from "./routes";
const app = express();
const handle = createRouteHandler(routes);
app.use(async (req, res, next) => {
const { matched } = await handle(req, res);
if (matched) {
return;
}
next();
});
app.listen(3000);import Koa from "koa";
import { createRouteHandler } from "@rest-rpc/node";
import { routes } from "./routes";
const app = new Koa();
const handle = createRouteHandler(routes);
app.use(async (ctx, next) => {
ctx.respond = false;
const { matched } = await handle(ctx.req, ctx.res);
if (matched) {
return;
}
await next();
});
app.listen(3000);Framework context
Pass application context as the third argument to handle:
declare module "@rest-rpc/node" {
interface DefaultContext {
user: { id: string };
}
}
const result = await handle(req, res, { user });
Handlers also receive context.signal. It is aborted when the request aborts
or the response closes prematurely. Add req or res to the context when
handlers need the native objects.
Options
type CreateNodeHandlerOptions = {
bodyParser?: NodeBodyParser;
requestValidationErrorHandler?: RequestValidationErrorHandler;
responseValidationErrorHandler?: ResponseValidationErrorHandler;
};
Body parsing
const handle = createRouteHandler(routes, {
bodyParser: async (request) => readCustomBody(request),
});
Error handling
Request validation errors default to a 400 response. Response contract validation errors default to a generic 500 response. The validation handlers receive the native Node request and response. Other errors reject the handler promise and should be handled at the server boundary.
const handle = createRouteHandler(routes, {
requestValidationErrorHandler: (error, req, res) => {
res.statusCode = 422;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ code: "VALIDATION_ERROR", issues: error.issues }));
},
responseValidationErrorHandler: (_error, req, res) => {
res.statusCode = 500;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ code: "INVALID_RESPONSE" }));
},
});
const server = createServer(async (req, res) => {
try {
await handle(req, res);
} catch (error) {
res.statusCode = 500;
res.end(JSON.stringify({ code: "INTERNAL_SERVER_ERROR" }));
}
});
server.listen(3000);