Hono Framework Guide — TypeScript API for Edge and Node.js 2024
Advertisement
Introduction
Why This Matters
Hono is the breakout web framework of 2024. At only ~14KB, it outperforms most frameworks on benchmark throughput, runs across every JavaScript runtime without changes, and ships with TypeScript types so good that its RPC client can infer request and response types without manual schema definitions.
For teams building API backends that need to run on Cloudflare Workers, Vercel Edge Functions, or Bun — while keeping a Node.js fallback — Hono is the only framework that works equally well on all of them. Its validator integration and typed c.req and c.json() make building fully type-safe APIs faster than any alternative.
In 2024, Hono is increasingly used as a drop-in replacement for Express in new projects where edge deployment is on the roadmap.
Setup
# Node.js
npm create hono@latest my-api -- --template nodejs
# Cloudflare Workers
npm create hono@latest my-worker -- --template cloudflare-workers
# Bun
npm create hono@latest my-api -- --template bunBasic Application
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { secureHeaders } from 'hono/secure-headers';
const app = new Hono();
// Built-in middleware
app.use('*', logger());
app.use('*', cors());
app.use('*', secureHeaders());
// Routes
app.get('/', (c) => c.json({ status: 'ok' }));
app.get('/health', (c) => {
return c.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
export default app;Typed Routing with Variables
import { Hono } from 'hono';
const app = new Hono();
// Path parameters — typed by default
app.get('/users/:id', (c) => {
const id = c.req.param('id'); // string
return c.json({ userId: id });
});
// Query parameters
app.get('/users', (c) => {
const page = c.req.query('page') ?? '1';
const limit = c.req.query('limit') ?? '20';
return c.json({ page: parseInt(page), limit: parseInt(limit) });
});
// Multiple path params
app.get('/organizations/:orgId/teams/:teamId', (c) => {
const orgId = c.req.param('orgId');
const teamId = c.req.param('teamId');
return c.json({ orgId, teamId });
});Input Validation with Zod Validator
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const app = new Hono();
const createUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
password: z.string().min(8),
role: z.enum(['admin', 'user']).default('user'),
});
const listUsersSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
app.post(
'/users',
zValidator('json', createUserSchema),
async (c) => {
const { name, email, password, role } = c.req.valid('json'); // fully typed
// create user...
return c.json({ id: 'usr_1', name, email, role }, 201);
}
);
app.get(
'/users',
zValidator('query', listUsersSchema),
async (c) => {
const { page, limit } = c.req.valid('query'); // page and limit are numbers
return c.json({ items: [], page, limit, total: 0 });
}
);Type-Safe RPC with Hono Client
Hono's most powerful feature: end-to-end type safety between server and client without any code generation.
// server.ts
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const userSchema = z.object({
name: z.string(),
email: z.string().email(),
});
const usersRoute = new Hono()
.get('/', async (c) => {
return c.json({ users: [{ id: '1', name: 'Alice' }] });
})
.post('/', zValidator('json', userSchema), async (c) => {
const data = c.req.valid('json');
return c.json({ id: 'usr_new', ...data }, 201);
});
const app = new Hono().route('/users', usersRoute);
export type AppType = typeof app;
export default app;// client.ts — works in any TypeScript project
import { hc } from 'hono/client';
import type { AppType } from './server';
const client = hc<AppType>('http://localhost:3000');
// Fully typed — no manual type imports needed
const res = await client.users.$get();
const data = await res.json(); // { users: Array<{ id: string; name: string }> }
const newUser = await client.users.$post({
json: { name: 'Bob', email: 'bob@example.com' },
});
const created = await newUser.json(); // { id: string; name: string; email: string }Middleware and Context
import { Hono, Context, Next } from 'hono';
import { createMiddleware } from 'hono/factory';
// Type-safe context variables via generics
type Variables = {
userId: string;
userRole: 'admin' | 'user';
};
const app = new Hono<{ Variables: Variables }>();
// Authentication middleware
const authenticate = createMiddleware<{ Variables: Variables }>(
async (c, next) => {
const authHeader = c.req.header('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401);
}
const token = authHeader.slice(7);
// validate token...
c.set('userId', 'usr_123');
c.set('userRole', 'user');
await next();
}
);
app.use('/api/*', authenticate);
app.get('/api/profile', (c) => {
const userId = c.get('userId'); // string — typed
const role = c.get('userRole'); // 'admin' | 'user' — typed
return c.json({ userId, role });
});Error Handling
import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
const app = new Hono();
// Global error handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status);
}
console.error('[Error]', err);
return c.json({ error: 'Internal server error' }, 500);
});
// 404 handler
app.notFound((c) => {
return c.json({ error: `Route ${c.req.path} not found` }, 404);
});
// Throw in a route
app.get('/protected', (c) => {
throw new HTTPException(403, { message: 'Forbidden' });
});Common Mistakes
- Calling
c.json()afterreturn— Hono returns the response from the return value - Forgetting that Hono runs in edge environments where Node.js built-ins like
fsare unavailable - Using
app.use()after route definitions — middleware must come before the routes it should apply to - Not typing context variables — always use Hono's generic to define your
Variablesshape - Importing Node.js-specific packages in Cloudflare Worker deployments
Best Practices
- Use
@hono/zod-validatorfor all input validation — it integrates withc.req.valid()for type safety - Use Hono's RPC pattern with
hc<AppType>for type-safe API clients instead of hand-written fetch calls - Group routes using
app.route()and export the combined type asAppTypefor the client - Use context variables (
c.set/c.get) with typed generics instead of middleware extending request objects - Test Hono apps with the built-in test helper to avoid starting a real HTTP server
Key Takeaways
- Hono runs natively on Cloudflare Workers, Deno, Bun, and Node.js — one codebase, any runtime
c.req.valid('json')returns fully typed request data after Zod validation — no manual casting- Hono's RPC with
hc<AppType>gives end-to-end type safety from server route to client call - Context variables are typed via the
Hono<{ Variables: T }>generic — no untypedreq.userhacks - The
@hono/zod-validatormiddleware validates and types request body, query, params, and headers - Hono's bundle size of ~14KB makes it ideal for edge deployments with cold-start constraints
- Built-in middleware covers logging, CORS, security headers, compression, and bearer auth
app.route('/prefix', subApp)composes multiple Hono instances — each can have its own middleware scope
Advertisement