Hono Framework Guide — TypeScript API for Edge and Node.js 2024

Sanjeev SharmaSanjeev Sharma
6 min read

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 bun

Basic 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&lt;{ Variables: Variables }>();
 
// Authentication middleware
const authenticate = createMiddleware&lt;{ 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() after return — Hono returns the response from the return value
  • Forgetting that Hono runs in edge environments where Node.js built-ins like fs are 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 Variables shape
  • Importing Node.js-specific packages in Cloudflare Worker deployments

Best Practices

  • Use @hono/zod-validator for all input validation — it integrates with c.req.valid() for type safety
  • Use Hono's RPC pattern with hc&lt;AppType> for type-safe API clients instead of hand-written fetch calls
  • Group routes using app.route() and export the combined type as AppType for 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&lt;AppType> gives end-to-end type safety from server route to client call
  • Context variables are typed via the Hono&lt;{ Variables: T }> generic — no untyped req.user hacks
  • The @hono/zod-validator middleware 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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading