| name | hono |
| description | 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, or hc() RPC client
How It Works
Step 1: Project Setup
Cloudflare Workers (recommended for edge):
npm create hono@latest my-api
cd my-api
npm install
npm run dev
npm run deploy
Bun / Node.js:
mkdir my-api && cd my-api
bun init
bun add hono
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();
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 }));
app.get('/posts/:id', async c => {
const id = c.req.param('id');
const format = c.req.query('format') ?? 'json';
return c.json({ id, format });
});
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();
app.use('*', logger());
app.use('/api/*', cors({ origin: 'https://myapp.com' }));
app.use('/api/admin/*', bearerAuth({ token: process.env.API_TOKEN! }));
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 => {
const body = await c.req.json<{ name: string; email: string }>();
const form = await c.req.formData();
const text = await c.req.text();
const auth = c.req.header('authorization');
const token = getCookie(c, 'session');
return c.json({ ok: true });
return c.text('hello');
return c.html('<h1>Hello</h1>');
return c.redirect('/dashboard', 302);
return new Response(stream, { status: 200 });
});
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');
const post = await db.post.create({ data });
return c.json(post, 201);
}
);
Step 6: Route Groups and App Composition
import { Hono } from 'hono';
const posts = new Hono();
posts.get('/', async c => { });
posts.post('/', async c => { });
posts.get('/:id', async c => { });
export default posts;
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:
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;
import { hc } from 'hono/client';
import type { PostsType } from '../server/routes/posts';
const client = hc<PostsType>('/api/posts');
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
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
zValidator for all request body, query, and param validation
- ✅ Type Cloudflare Workers bindings with the
Bindings generic: new Hono<{ Bindings: Env }>()
- ✅ Use the RPC client (
hc) when your frontend and backend share the same repo
- ✅ Prefer returning
c.json()/c.text() over new 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 keep c.get() type-safe
Security & Safety Notes
- Always validate input with
zValidator before using data from requests.
- Use Hono's built-in
csrf middleware on mutation endpoints when serving HTML/forms.
- For Cloudflare Workers, store secrets in
wrangler.toml [vars] (non-secret) or wrangler secret put (secret) — never hardcode them in source.
- When using
bearerAuth or jwt, 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: Always return a response from handlers: return c.json(...) not just c.json(...).
-
Problem: Middleware runs after the response is sent
Solution: Call await next() before post-response logic; Hono runs code after next() as the response travels back up the chain.
-
Problem: c.env is undefined on Node.js
Solution: Cloudflare env bindings only exist in Workers. Use process.env on 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)
Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.