Hono vs itty-router: Edge-First API Frameworks Compared
TL;DR
Hono is the better choice for most edge projects. itty-router wins on bundle size for ultra-minimal Workers. Hono (~1.8M weekly downloads) provides a full-featured framework with middleware, validation, and a growing ecosystem. itty-router (~200K) is a 500-byte router — nothing more. Choose itty-router when you need the absolute minimum footprint and don't need middleware. Choose Hono for everything else.
Key Takeaways
- Hono: ~1.8M weekly downloads — itty-router: ~200K (npm, March 2026)
- itty-router is ~500 bytes — Hono is ~12KB (still tiny by Node.js standards)
- Hono has TypeScript generics — itty-router has minimal types
- Hono has middleware ecosystem — itty-router has none built-in
- Both target Cloudflare Workers — but Hono also targets Node, Bun, Deno
Context: Edge Runtime Constraints
Cloudflare Workers has a 1MB compressed script size limit (free tier: 1MB total, paid: 10MB). This constraint doesn't matter for most Hono applications (12KB overhead leaves plenty of room), but it's why itty-router exists — when you're counting bytes.
Workers also start cold much faster than Node.js functions, making the startup cost of frameworks less relevant. The relevant metrics are: bundle size, request throughput, and DX.
itty-router: Ruthlessly Minimal
// itty-router — the entire router in ~500 bytes
import { Router } from 'itty-router';
const router = Router();
router.get('/users/:id', async ({ params, env }) => {
const user = await env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
).bind(params.id).first();
if (!user) return new Response('Not found', { status: 404 });
return Response.json(user);
});
router.post('/users', async (request, env) => {
const body = await request.json();
// Manual validation required
if (!body.name || !body.email) {
return new Response('Missing fields', { status: 400 });
}
await env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
.bind(body.name, body.email)
.run();
return new Response('Created', { status: 201 });
});
// Cloudflare Workers export
export default {
fetch: router.handle,
};
itty-router is basically app.get(path, handler). That's it. No middleware, no validation, no built-in error handling, no TypeScript inference. You get a router and you build everything else yourself.
Hono: Full-Featured Edge Framework
// Hono — full framework, still ~12KB total
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
type Env = {
Bindings: { DB: D1Database };
};
const app = new Hono<Env>();
app.get('/users/:id', async (c) => {
const id = c.req.param('id');
const stmt = c.env.DB.prepare('SELECT * FROM users WHERE id = ?');
const user = await stmt.bind(id).first();
if (!user) return c.json({ error: 'Not found' }, 404);
return c.json(user);
});
app.post('/users',
zValidator('json', z.object({
name: z.string().min(1),
email: z.string().email(),
})),
async (c) => {
const body = c.req.valid('json'); // Typed: { name: string, email: string }
await c.env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
.bind(body.name, body.email)
.run();
return c.json({ success: true }, 201);
}
);
// Middleware
app.use('*', async (c, next) => {
const apiKey = c.req.header('X-API-Key');
if (!apiKey) return c.json({ error: 'Unauthorized' }, 401);
await next();
});
export default app;
Bundle Size Comparison
itty-router: ~500 bytes (minified)
hono/tiny: ~3KB
hono: ~12KB (with middleware hooks)
express: ~250KB+ with deps
fastify: ~350KB+ with deps
For Cloudflare Workers where you're often compiling the entire worker to a single JS bundle, 12KB vs 500 bytes is genuinely relevant. A Worker that does 3 things with D1 and needs the router only might legitimately benefit from itty-router's smaller footprint.
TypeScript Support
// itty-router TypeScript — minimal
import { Router, IRequest } from 'itty-router';
interface Env {
DB: D1Database;
}
const router = Router();
// Request type isn't strongly typed from the route definition
router.get('/users/:id', async (req: IRequest, env: Env) => {
// req.params.id is string — basic typing works
const id: string = req.params.id;
// But no inference of what the handler returns
});
// Hono TypeScript — full generics
import { Hono } from 'hono';
const app = new Hono<{
Bindings: { DB: D1Database };
Variables: { userId: string };
}>();
// Full type inference through the entire request lifecycle
app.get('/users/:id', (c) => {
const id = c.req.param('id'); // string
const userId = c.get('userId'); // string (from middleware)
// Return type is inferred from c.json()
return c.json({ id, userId });
});
Middleware Ecosystem
| Feature | Hono | itty-router |
|---|---|---|
| CORS | @hono/cors | Manual |
| JWT auth | hono/jwt | Manual |
| Rate limiting | hono/rate-limiter | Manual |
| Request validation | @hono/zod-validator | Manual |
| Response compression | hono/compress | Manual |
| Static files | Built-in | Manual |
| Swagger UI | @hono/swagger-ui | Manual |
itty-router has essentially no middleware ecosystem. You implement everything yourself.
When to Choose
Choose itty-router when:
- Bundle size is genuinely the primary constraint
- You're building a single-purpose Worker (proxy, redirects, simple CRUD)
- You don't need middleware and are comfortable implementing auth/validation manually
- You're building a prototype and want zero dependencies
Choose Hono when:
- Building any production-grade API on edge runtimes
- TypeScript type safety matters
- You need middleware (auth, CORS, validation)
- The project will grow beyond 3-5 routes
- You might deploy to multiple runtimes (Workers + Node.js)
For essentially any real application, Hono's 12KB is worth the trade for DX and ecosystem depth.
Compare Hono and itty-router package health on PkgPulse.
See the live comparison
View hono vs. itty router on PkgPulse →