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

Hono

Use rest-rpc with Hono

Install

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

Usage

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

const app = new Hono();
const api = new Hono();
const todos = new Map([["todo_1", { id: "todo_1", title: "Write docs" }]]);

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(api, routes);
app.route("/api", api);
export default app;

registerRoutes() accepts a Hono app. Mount it with app.route() to serve routes under a prefix.

Framework Context

Handlers and middleware receive the Hono context c in addition to the common handler arguments:

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

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

Options

type RegisterRoutesOptions<TEnv extends Env = Env> = {
	bodyCodecs?: readonly BodyCodec<HonoRequest>[];
	disableRequestValidation?: boolean;
	disableResponseValidation?: boolean;
	requestBodyLimit?: number;
	requestValidationErrorHandler?: RequestValidationErrorHandler<TEnv>;
	responseValidationErrorHandler?: ResponseValidationErrorHandler<TEnv>;
	middleware?: ExtendedHonoMiddleware<TEnv>[];
};

Middleware

Use the middleware option to run Hono middleware only for rest-rpc routes. Each function has the Hono 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. It runs before rest-rpc decodes and validates the request, and before route middleware.

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

Request and Response Bodies

The adapter decodes request bodies with the default codecs, so no body-parsing middleware is needed. Built-in decoding reads at most requestBodyLimit bytes, 1 MiB by default. A custom codec’s deserialize receives the HonoRequest (c.req) and is not subject to requestBodyLimit.

Handlers can return any value supported by the default codecs. A custom serializer’s body must be a Fetch Response body, such as a string, Blob, FormData, byte array, or ReadableStream.

Error Handling

Use requestValidationErrorHandler and responseValidationErrorHandler to replace the default validation error responses. Other errors use Hono’s error handler.

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

app.onError((error, c) => {
	return c.json({ code: "INTERNAL_SERVER_ERROR" }, 500);
});

Was this page helpful?