Adapters

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 hono
bun add @ts-kizuna/hono hono
npm install @ts-kizuna/hono hono

Setup

The standard setup:

  • new KizunaServer() binds the contract to a server handle
  • server.router binds typed implementations to the contract
  • server.api combines the router and guards into an API object
  • api.mount(app) mounts the API on a Hono app
src/server/server.ts
import { KizunaServer } from '@ts-kizuna/hono';
import { contract } from '@shared/contract';

export const server = new KizunaServer(contract);
src/server/router/index.ts
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,
            };
        },
    },
});
src/server/api.ts
import { server } from './server';
import { router } from './router';

export const api = server.api({
    router,
});
src/index.ts
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.

src/server/router/index.ts
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.

src/server/guards.ts
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,
});
OptionDefaultDescription
responseValidationfalseValidate handler return values against response schemas. Enable in development.
formatErrornoneReshape 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.

Reference

On this page