Skip to content
rest-rpc
Esc
navigateopen⌘Jpreview
On this page

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

Was this page helpful?