Hono
Mount a ts-kizuna API on a Hono application.
@ts-kizuna/hono connects a ts-kizuna API to a Hono application. Hono runs on Cloudflare Workers, Deno, Bun, Node.js, and other runtimes, and the adapter works the same everywhere.
Requires Hono >= 4.
pnpm add @ts-kizuna/hono honobun add @ts-kizuna/hono hononpm install @ts-kizuna/hono honoSetup
The standard setup:
new KizunaServer()binds the contract to aserverhandleserver.routerbinds typed implementations to the contractserver.apicombines the router and guards into an API objectapi.mount(app)mounts the API on a Hono app
import { KizunaServer } from '@ts-kizuna/hono';
import { contract } from '@shared/contract';
export const server = new KizunaServer(contract);import { server } from './server';
export const router = server.router({
users: {
listUsers: async ({ query }) => {
const found = await db.users.findMany({
skip: (query.page - 1) * query.limit,
take: query.limit,
});
return {
status: 200,
body: {
users: found,
total: await db.users.count(),
},
};
},
createUser: async ({ body }) => {
const user = await db.users.create(body);
return {
status: 201,
body: user,
};
},
getUser: async ({ params }) => {
const user = await db.users.findById(params.id);
if (!user) {
return {
status: 404,
body: {
detail: 'Not found',
},
};
}
return {
status: 200,
body: user,
};
},
},
});import { server } from './server';
import { router } from './router';
export const api = server.api({
router,
});import { Hono } from 'hono';
import { api } from './server/api';
const app = new Hono();
api.mount(app);
export default app;Handler context
Each handler receives { params, query, body, headers, c }. The c is the Hono Context object, useful when you need to access cookies, environment bindings, or other Hono-specific features.
getUser: async ({ params, c }) => {
const token = c.req.header('Authorization');
const user = await db.users.findById(params.id);
return {
status: 200,
body: user,
};
},Environment bindings
On runtimes like Cloudflare Workers, environment bindings are available on the Hono context as c.env.
import { server } from './server';
export const router = server.router({
users: {
getUser: async ({ params, c }) => {
const user = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(params.id).first();
return {
status: 200,
body: user,
};
},
},
});Guards
One guard per identity. The credential arrives extracted and typed, alongside the native context c. See the Auth guide.
import { server } from './server';
export const requireUser = server.guard('user', async ({ bearer, deny }) => {
const session = bearer ? await verifySession(bearer.token) : undefined;
if (!session) {
return deny(401, 'Unauthorized');
}
return {
userId: session.userId,
};
});Middleware
For request-scoped values handlers need, use Kizuna.requestContext. For other middleware, such as logging, rate limiting, and caching headers, use Hono's own app.use. Authentication belongs in guards.
Options
api.mount(app, {
responseValidation: false,
});| Option | Default | Description |
|---|---|---|
responseValidation | false | Validate handler return values against response schemas. Enable in development. |
formatError | none | Reshape error (>= 400) response bytes for migrating clients. Most don't need it (use Problem Details extension members). See Migrating an existing API. |
RouteHandler and Router types
Annotate handlers individually or as a whole tree:
import type { RouteHandler, Router } from '@ts-kizuna/hono';
import { contract } from '@shared/contract';
// Single route
const getUser: RouteHandler<typeof contract.routes.users.getUser> = async ({ params }) => {
const user = await db.users.findById(params.id);
if (!user) {
return {
status: 404,
body: {
detail: 'Not found',
},
};
}
return {
status: 200,
body: user,
};
};
// Whole handler tree
const router: Router<typeof contract> = { ... };RouteHandler carries the route's auth and request context, nested groups included.