Hono
Register contract handlers in a Hono app.
Use @rest-rpc/hono to implement and register contract routes in a Hono app.
The adapter registers routes by HTTP method and path, then delegates validation and response handling to the shared server layer.
Install
pnpm add @rest-rpc/core @rest-rpc/hono hono
Basic Setup
import { registerRoutes, router } from "@rest-rpc/hono";
import { Hono } from "hono";
import { api } from "./contract";
const app = new Hono();
const routes = router(api, {
todos: {
get({ id, context }) {
const userAgent = context.c.req.header("user-agent");
void userAgent;
const todo = todos.get(id);
if (!todo) {
return {
status: 404,
body: {
code: "TODO_NOT_FOUND",
},
};
}
return todo;
},
},
});
registerRoutes(app, routes);
export default app;
Handler Input
Handlers receive flattened request input where context contains the Hono context object.
get({ id, "x-request-id": requestId, context }) {
return loadTodo({
id,
requestId,
c: context.c,
});
}
Handler Returns
Handlers can always return an explicit response envelope.
return {
status: 404,
body: {
code: "TODO_NOT_FOUND",
},
};
If a route has exactly one successful response, the handler can return the success body directly.
return todo;
You can also use ContractResponseError to throw a declared error response when that’s more convenient.
throw new ContractResponseError(api.todos.get, {
status: 404,
body: {
code: "TODO_NOT_FOUND",
},
});
Error Handling
Use errorHandlers to customize server-generated error responses. Declared
responses returned from handlers, including ContractResponseError, do not run
through these hooks.
registerRoutes(app, routes, {
errorHandlers: {
onRequestValidationError: ({ issues }) => ({
status: 422,
body: { code: "VALIDATION_ERROR", issues },
}),
onUnhandledError: () => ({
status: 500,
body: { code: "INTERNAL_SERVER_ERROR" },
}),
},
});
Middleware
rest-rpc does not own middleware. Use Hono middleware the same way you would
in any Hono app.
When middleware needs to behave differently for different contract routes, use
createRouteMatcher to match the incoming request back to the contract route.
const matchContractRoute = createRouteMatcher(api);
app.use(async (c, next) => {
const match = matchContractRoute({
method: c.req.method,
path: c.req.path,
});
if (!match?.route.metadata?.requiresAuth) {
return next();
}
const session = await loadSession(c.req.header("authorization"));
if (!session) {
return c.json({ code: "UNAUTHORIZED" }, 401);
}
c.set("session", session);
return next();
});
Handlers can read values added by middleware through context.c.
const routes = router(api, {
todos: {
get({ id, context }) {
return loadTodoForSession({
id,
session: context.c.get("session"),
});
},
},
});
Body Parsing
For normal JSON routes, the adapter can read JSON request bodies through Hono’s request APIs.
Use parseBody when your app needs custom body parsing.
import { isCustomBody } from "@rest-rpc/core";
registerRoutes(app, routes, {
parseBody: async ({ c, body }) => {
if (isCustomBody(body)) {
return c.req.arrayBuffer();
}
return c.req.json();
},
});
WebSockets
Hono gets upgradeWebSocket from the Hono runtime adapter. Create the Hono app
normally, import upgradeWebSocket from your runtime, then pass both to
registerRoutes.
beforeUpgrade runs after request validation and before the socket is accepted.
Return an upgrade rejection to deny the connection, or read and attach data to
context.c for the WebSocket handler.
import { Hono } from "hono";
import { upgradeWebSocket } from "hono/cloudflare-workers";
import { registerRoutes } from "@rest-rpc/hono";
const honoApp = new Hono();
registerRoutes(honoApp, routes, {
webSocket: {
upgradeWebSocket,
beforeUpgrade({ context, route, request }) {
context.c;
route;
request;
return undefined;
},
},
});
See WebSockets for the contract and handler model.