hono
Build ultra-fast web APIs and full-stack apps with Hono — runs on Cloudflare Workers, Deno, Bun, Node.js, and any WinterCG-compatible runtime.
- category
- backend
- risk
- safe
- source
- community
- date added
- 2026-03-18
- author
- suhaibjanjua
- tags
- [hono, edge, cloudflare-workers, bun, deno, api, typescript, web-standards]
- tools
- [claude, cursor, gemini]
Hono Web Framework
Overview
Hono (炎, "flame" in Japanese) is a small, ultrafast web framework built on Web Standards (Request/Response/fetch). It runs anywhere: Cloudflare Workers, Deno Deploy, Bun, Node.js, AWS Lambda, and any WinterCG-compatible runtime — with the same code. Hono's router is one of the fastest available, and its middleware system, built-in JSX support, and RPC client make it a strong choice for edge APIs, BFFs, and lightweight full-stack apps.
When to Use This Skill
- Use when building a REST or RPC API for edge deployment (Cloudflare Workers, Deno Deploy)
- Use when you need a minimal but type-safe server framework for Bun or Node.js
- Use when building a Backend for Frontend (BFF) layer with low latency requirements
- Use when migrating from Express but wanting better TypeScript support and edge compatibility
- Use when the user asks about Hono routing, middleware,
c.req,c.json, orhc()RPC client
How It Works
Step 1: Project Setup
Cloudflare Workers (recommended for edge):
npm create hono@latest my-api # Select: cloudflare-workers cd my-api npm install npm run dev # Wrangler local dev npm run deploy # Deploy to Cloudflare
Bun / Node.js:
mkdir my-api && cd my-api bun init bun add hono
// src/index.ts (Bun) import { Hono } from 'hono'; const app = new Hono(); app.get('/', c => c.text('Hello Hono!')); export default { port: 3000, fetch: app.fetch, };
Step 2: Routing
import { Hono } from 'hono'; const app = new Hono(); // Basic methods app.get('/posts', c => c.json({ posts: [] })); app.post('/posts', c => c.json({ created: true }, 201)); app.put('/posts/:id', c => c.json({ updated: true })); app.delete('/posts/:id', c => c.json({ deleted: true })); // Route params and query strings app.get('/posts/:id', async c => { const id = c.req.param('id'); const format = c.req.query('format') ?? 'json'; return c.json({ id, format }); }); // Wildcard app.get('/static/*', c => c.text('static file')); export default app;
Chained routing:
app .get('/users', listUsers) .post('/users', createUser) .get('/users/:id', getUser) .patch('/users/:id', updateUser) .delete('/users/:id', deleteUser);
Step 3: Middleware
Hono middleware works exactly like fetch interceptors — before and after handlers:
import { Hono } from 'hono'; import { logger } from 'hono/logger'; import { cors } from 'hono/cors'; import { bearerAuth } from 'hono/bearer-auth'; const app = new Hono(); // Built-in middleware app.use('*', logger()); app.use('/api/*', cors({ origin: 'https://myapp.com' })); app.use('/api/admin/*', bearerAuth({ token: process.env.API_TOKEN! })); // Custom middleware app.use('*', async (c, next) => { c.set('requestId', crypto.randomUUID()); await next(); c.header('X-Request-Id', c.get('requestId')); });
Available built-in middleware: logger, cors, csrf, etag, cache, basicAuth, bearerAuth, jwt, compress, bodyLimit, timeout, prettyJSON, secureHeaders.
Step 4: Request and Response Helpers
app.post('/submit', async c => { // Parse body const body = await c.req.json<{ name: string; email: string }>(); const form = await c.req.formData(); const text = await c.req.text(); // Headers and cookies const auth = c.req.header('authorization'); const token = getCookie(c, 'session'); // Responses return c.json({ ok: true }); // JSON return c.text('hello'); // plain text return c.html('<h1>Hello</h1>'); // HTML return c.redirect('/dashboard', 302); // redirect return new Response(stream, { status: 200 }); // raw Response });
Step 5: Zod Validator Middleware
import { zValidator } from '@hono/zod-validator'; import { z } from 'zod'; const createPostSchema = z.object({ title: z.string().min(1).max(200), body: z.string().min(1), tags: z.array(z.string()).default([]), }); app.post( '/posts', zValidator('json', createPostSchema), async c => { const data = c.req.valid('json'); // fully typed const post = await db.post.create({ data }); return c.json(post, 201); } );
Step 6: Route Groups and App Composition
// src/routes/posts.ts import { Hono } from 'hono'; const posts = new Hono(); posts.get('/', async c => { /* list posts */ }); posts.post('/', async c => { /* create post */ }); posts.get('/:id', async c => { /* get post */ }); export default posts;
// src/index.ts import { Hono } from 'hono'; import posts from './routes/posts'; import users from './routes/users'; const app = new Hono().basePath('/api'); app.route('/posts', posts); app.route('/users', users); export default app;
Step 7: RPC Client (End-to-End Type Safety)
Hono's RPC mode exports route types that the hc client consumes — similar to tRPC but using fetch conventions:
// server: src/routes/posts.ts import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; import { z } from 'zod'; const posts = new Hono() .get('/', c => c.json({ posts: [{ id: '1', title: 'Hello' }] })) .post( '/', zValidator('json', z.object({ title: z.string() })), async c => { const { title } = c.req.valid('json'); return c.json({ id: '2', title }, 201); } ); export default posts; export type PostsType = typeof posts;
// client: src/client.ts import { hc } from 'hono/client'; import type { PostsType } from '../server/routes/posts'; const client = hc<PostsType>('/api/posts'); // Fully typed — autocomplete on routes, params, and responses const { posts } = await client.$get().json(); const newPost = await client.$post({ json: { title: 'New Post' } }).json();
Examples
Example 1: JWT Auth Middleware
import { Hono } from 'hono'; import { jwt, sign } from 'hono/jwt'; const app = new Hono(); const SECRET = process.env.JWT_SECRET!; app.post('/login', async c => { const { email, password } = await c.req.json(); const user = await validateUser(email, password); if (!user) return c.json({ error: 'Invalid credentials' }, 401); const token = await sign({ sub: user.id, exp: Math.floor(Date.now() / 1000) + 3600 }, SECRET); return c.json({ token }); }); app.use('/api/*', jwt({ secret: SECRET })); app.get('/api/me', async c => { const payload = c.get('jwtPayload'); const user = await getUserById(payload.sub); return c.json(user); }); export default app;
Example 2: Cloudflare Workers with D1 Database
// src/index.ts import { Hono } from 'hono'; type Bindings = { DB: D1Database; API_TOKEN: string; }; const app = new Hono<{ Bindings: Bindings }>(); app.get('/users', async c => { const { results } = await c.env.DB.prepare('SELECT * FROM users LIMIT 50').all(); return c.json(results); }); app.post('/users', async c => { const { name, email } = await c.req.json(); await c.env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)') .bind(name, email) .run(); return c.json({ created: true }, 201); }); export default app;
Example 3: Streaming Response
import { stream, streamText } from 'hono/streaming'; app.get('/stream', c => streamText(c, async stream => { for (const chunk of ['Hello', ' ', 'World']) { await stream.write(chunk); await stream.sleep(100); } }) );
Best Practices
- ✅ Use route groups (sub-apps) to keep handlers in separate files —
app.route('/users', usersRouter) - ✅ Use
zValidatorfor all request body, query, and param validation - ✅ Type Cloudflare Workers bindings with the
Bindingsgeneric:new Hono<{ Bindings: Env }>() - ✅ Use the RPC client (
hc) when your frontend and backend share the same repo - ✅ Prefer returning
c.json()/c.text()overnew Response()for cleaner code - ❌ Don't use Node.js-specific APIs (
fs,path,process) if you want edge portability - ❌ Don't add heavy dependencies — Hono's value is its tiny footprint on edge runtimes
- ❌ Don't skip middleware typing — use generics (
Variables,Bindings) to keepc.get()type-safe
Security & Safety Notes
- Always validate input with
zValidatorbefore using data from requests. - Use Hono's built-in
csrfmiddleware on mutation endpoints when serving HTML/forms. - For Cloudflare Workers, store secrets in
wrangler.toml[vars](non-secret) orwrangler secret put(secret) — never hardcode them in source. - When using
bearerAuthorjwt, ensure tokens are validated server-side — do not trust client-provided user IDs. - Rate-limit sensitive endpoints (auth, password reset) with Cloudflare Rate Limiting or a custom middleware.
Common Pitfalls
-
Problem: Handler returns
undefined— response is empty Solution: Alwaysreturna response from handlers:return c.json(...)not justc.json(...). -
Problem: Middleware runs after the response is sent Solution: Call
await next()before post-response logic; Hono runs code afternext()as the response travels back up the chain. -
Problem:
c.envis undefined on Node.js Solution: Cloudflareenvbindings only exist in Workers. Useprocess.envon Node.js. -
Problem: Route not matching — gets a 404 Solution: Check that
app.route('/prefix', subRouter)uses the same prefix your client calls. Sub-routers should not repeat the prefix in their own routes.
Related Skills
@cloudflare-workers-expert— Deep dive into Cloudflare Workers platform specifics@trpc-fullstack— Alternative RPC approach for TypeScript full-stack apps@zod-validation-expert— Detailed Zod schema patterns used with@hono/zod-validator@nodejs-backend-patterns— When you need a Node.js-specific backend (not edge)