Core concepts
Hono & middleware
src/server.ts is a Hono sub-app — middleware, endpoints, end-to-end types, error reporting.
There is a whole Hono app under the pages, and it is yours. src/server.ts may default-export a sub-app:
any method, streaming, cookies, middleware.
import { Hono } from 'hono';
import { trimTrailingSlash } from 'hono/trailing-slash';
export type AppEnv = { Variables: { requestId: string } };
const server = new Hono<AppEnv>();
server.use(trimTrailingSlash({ alwaysRedirect: true }));
server.use('*', async (c, next) => {
c.set('requestId', crypto.randomUUID());
await next();
});
server.get('/api/health', (c) => c.json({ status: 'ok', requestId: c.var.requestId }));
export default server;
export type AppType = typeof server;
The sub-app is mounted at / ahead of the page routes, so its middleware — auth, logging,
trailing-slash — wraps page requests too.
The flip side: a terminal handler at the same path as a page route shadows the page. Middleware
that calls next() is fine; a handler that returns a response is not.
Typing the context
The Env given to the Hono app is the same one that types ctx on a page. Pass it to PageProps and
ctx.var is typed key by key instead of being an open record:
import type { PageProps } from '@rshono/core';
import type { AppEnv } from '../server';
export default function Home({ ctx }: PageProps<'/', AppEnv>) {
return <p>Request {ctx.var.requestId}</p>; // typed
}
End-to-end types for a client
export type AppType = typeof server gives typed paths, params and responses with hono/client,
checked against the handlers themselves:
import { hc } from 'hono/client';
import type { AppType } from './server';
const client = hc<AppType>('/');
const res = await client.api.health.$get();
Error reporting
One handler, registered at the top level of src/server.ts, catches every error the framework sees — a
thrown action, a failed render, SSR falling over, anything reaching the top-level handler:
import { onServerError } from '@rshono/core/server';
onServerError((error, { source, request }) => {
Sentry.captureException(error, { tags: { source }, extra: { url: request.url } });
});
source is 'action' | 'render' | 'ssr' | 'request'. Errors keep going to stderr either way, and a
handler that throws is caught rather than failing the request.