Hono vs itty-router vs Worktop: Cloudflare Workers Frameworks 2026
·PkgPulse Team
TL;DR
Hono is the 2026 default for Cloudflare Workers APIs — 14KB bundle, Express-like API, full TypeScript, runs everywhere (Workers, Node.js, Bun, Deno). itty-router is 580 bytes — useful when bundle size is critical (IoT edge, large function counts). Worktop (by lukeed) is largely superseded by Hono. For new Workers projects: Hono. For ultra-minimal routing: itty-router v4.
Key Takeaways
- Hono: 14KB, JSX, middleware ecosystem, runs on 5+ runtimes
- itty-router v4: 580 bytes, chainable routing, for extreme bundle constraints
- Worktop: ~5KB, now largely abandoned (use Hono instead)
- Performance: All three are near-identical (Workers runtime is the bottleneck)
- TypeScript: Hono has first-class types; itty-router v4 improved TS support
- Ecosystem: Hono has 3M+ downloads/week; itty-router ~100K; Worktop abandoned
Downloads
| Package | Weekly Downloads | Trend |
|---|---|---|
hono | ~3M | ↑ Fast growing |
itty-router | ~100K | → Stable |
worktop | ~5K | ↓ Declining |
Hono: The Full-Featured Standard
// Cloudflare Worker with Hono:
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { bearerAuth } from 'hono/bearer-auth';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
type Bindings = {
DB: D1Database; // Cloudflare D1 SQLite
KV: KVNamespace; // Cloudflare KV
R2: R2Bucket; // Cloudflare R2
AI: Ai; // Workers AI
};
const app = new Hono<{ Bindings: Bindings }>();
// Middleware:
app.use('*', cors({ origin: ['https://yourapp.com'] }));
app.use('/api/*', bearerAuth({ token: 'secret-token' }));
// Route with validation:
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
app.post('/api/users', zValidator('json', createUserSchema), async (c) => {
const { email, name } = c.req.valid('json');
const db = c.env.DB; // Typed D1 binding
const result = await db
.prepare('INSERT INTO users (email, name) VALUES (?, ?) RETURNING id')
.bind(email, name)
.first<{ id: string }>();
return c.json({ id: result?.id, email, name }, 201);
});
// D1 query:
app.get('/api/users', async (c) => {
const users = await c.env.DB
.prepare('SELECT id, email, name FROM users ORDER BY created_at DESC LIMIT 100')
.all<{ id: string; email: string; name: string }>();
return c.json(users.results);
});
// KV cache:
app.get('/api/config', async (c) => {
const cached = await c.env.KV.get('app-config', { type: 'json' });
if (cached) return c.json(cached);
const config = { version: '1.0', features: ['dark-mode', 'analytics'] };
await c.env.KV.put('app-config', JSON.stringify(config), { expirationTtl: 3600 });
return c.json(config);
});
// Workers AI:
app.post('/api/summarize', async (c) => {
const { text } = await c.req.json();
const result = await c.env.AI.run('@cf/meta/llama-3-8b-instruct', {
messages: [{ role: 'user', content: `Summarize: ${text}` }],
});
return c.json({ summary: result.response });
});
export default app;
// wrangler.toml — Workers config:
// name = "my-api"
// main = "src/index.ts"
// compatibility_date = "2024-09-23"
//
// [[d1_databases]]
// binding = "DB"
// database_name = "my-db"
// database_id = "xxx"
//
// [[kv_namespaces]]
// binding = "KV"
// id = "xxx"
# Hono Workers commands:
npm create hono@latest my-app -- --template cloudflare-workers
wrangler dev # Local dev
wrangler deploy # Deploy to Workers
wrangler d1 execute DB --file=./schema.sql # Run migrations
itty-router v4: Ultra-Minimal
// itty-router v4 — 580 bytes:
import { AutoRouter, cors, error, json } from 'itty-router';
const router = AutoRouter({
// before: runs before matching
before: [cors()],
// catch: error handler
catch: (err) => error(500, err.message),
// finally: transform all responses
finally: [json],
});
router
.get('/api/users', async (request, env: Env) => {
const users = await env.DB
.prepare('SELECT * FROM users')
.all();
return users.results;
})
.post('/api/users', async (request: IRequest, env: Env) => {
const body = await request.json();
await env.DB
.prepare('INSERT INTO users (email, name) VALUES (?, ?)')
.bind(body.email, body.name)
.run();
return { success: true };
})
.get('/api/users/:id', async ({ params }: IRequest, env: Env) => {
const user = await env.DB
.prepare('SELECT * FROM users WHERE id = ?')
.bind(params.id)
.first();
return user ?? error(404, 'User not found');
});
export default router;
// itty-router v4 bundle size breakdown:
// AutoRouter: ~580 bytes (minified + gzipped)
// cors(): ~200 bytes
// json(): ~50 bytes
// TOTAL: ~830 bytes vs Hono's ~14KB
// When 580 bytes matters:
// → Cloudflare free tier: 1MB total per worker
// → Workers with many functions (each has size limit)
// → Hobbyist/personal projects where simplicity > features
Bundle Size Comparison
Framework bundle sizes (minified + gzipped):
hono: 14KB ← Full-featured
itty-router: 0.6KB ← Ultra-minimal
worktop: 5KB ← Largely abandoned
For context:
Express.js: ~200KB+ (Node.js only)
Fastify: ~50KB (Node.js only)
Workers CPU limits: 10ms (free), 30s (paid)
Workers bundle limit: 3MB (uncompressed) — size rarely matters
Multi-Runtime: Hono's Superpower
// Hono runs on multiple runtimes — same code:
// Cloudflare Workers:
export default app;
// Node.js (for local testing):
import { serve } from '@hono/node-server';
serve(app, (info) => {
console.log(`Server running on port ${info.port}`);
});
// Bun:
export default { port: 3000, fetch: app.fetch };
// Lambda (via @hono/aws-lambda):
import { handle } from '@hono/aws-lambda';
export const handler = handle(app);
// Edge functions (Vercel, Netlify):
export const config = { runtime: 'edge' };
export default app.fetch;
Hono JSX (Workers UI)
// Hono supports JSX for server-rendered HTML:
import { Hono } from 'hono';
import { html } from 'hono/html';
const app = new Hono();
const Layout = ({ children }: { children: any }) => html`
<!DOCTYPE html>
<html>
<head><title>My Workers App</title></head>
<body>${children}</body>
</html>
`;
app.get('/', (c) => {
return c.html(
<Layout>
<h1>Hello from Cloudflare Workers!</h1>
</Layout>
);
});
// Or with JSX transform (tsconfig: "jsx": "react-jsx"):
app.get('/users', async (c) => {
const users = await c.env.DB.prepare('SELECT * FROM users').all();
return c.html(
<html>
<body>
<ul>
{users.results.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</body>
</html>
);
});
Decision Guide
Use Hono if:
→ Building a real API (middleware, auth, validation)
→ Need to run same code on multiple runtimes
→ Want type-safe bindings (D1, KV, R2)
→ TypeScript-first project
→ Any new project in 2026
Use itty-router if:
→ Ultra-small bundle size is a hard requirement
→ Simple proxy/redirect logic
→ Reading/writing to KV without complex routing
→ Building edge functions for a CDN (not a full app)
Avoid Worktop:
→ No recent releases (last update 2022)
→ Hono does everything Worktop did, better
→ Even the author recommends moving to Hono
Compare Hono, itty-router, and Worktop download trends on PkgPulse.