Skip to main content

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

PackageWeekly DownloadsTrend
hono~3M↑ Fast growing
itty-router~100K→ Stable
worktop~5K↓ Declining

// 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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.