Skip to main content

Hono vs itty-router vs Worktop: Cloudflare Workers 2026

·PkgPulse Team
0

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

Middleware Architecture and Hono's Ecosystem

Hono's middleware system is one of its most underappreciated advantages over itty-router. Hono ships with a substantial set of production-ready built-in middleware in the hono/ subpath — CORS, Bearer authentication, Basic auth, JWT validation, rate limiting, request timing, Zod request validation, and more. These are not external packages you need to find and evaluate separately; they're maintained by the Hono team and always compatible with the current API. For building a real API on Cloudflare Workers, this means you can implement auth, validation, and CORS in minutes rather than assembling disparate packages.

The @hono/zod-validator middleware deserves particular attention. Unlike generic body-parsing middleware, it integrates Zod schemas directly into the route handler so that c.req.valid('json') returns a fully typed object inferred from your schema — no manual type assertions needed. This pattern scales naturally to OpenAPI documentation generation via @hono/zod-openapi, which lets you define routes once and get both runtime validation and OpenAPI 3.1 spec output automatically.

itty-router v4's AutoRouter provides before/catch/finally hooks that cover the most common middleware needs (CORS, error handling, JSON response formatting), but the ecosystem beyond that is sparse. Complex middleware chains, request body validation with TypeScript inference, or JWT verification all require custom implementation or hunting for community packages with varying quality.

Hono's multi-runtime capability also changes the testing story significantly. You can test Hono Workers apps using hono/testing with app.request() — no actual HTTP server required. Tests run fast and work identically in Vitest on Node.js and in the Cloudflare Workers test environment via Wrangler's vitest-pool-workers.

Cloudflare Workers Platform Integration Differences

When building on Cloudflare Workers, how a framework exposes platform bindings (D1, KV, R2, Queues, Workers AI) matters more than raw routing performance. Hono's generic Bindings type parameter gives you a clean way to declare your Worker's environment and get full TypeScript IntelliSense on c.env.DB, c.env.KV, etc. The type flows through every route handler and middleware in the app, so you never get a type error when accessing a binding that's declared in wrangler.toml.

itty-router handles bindings through function parameters — the Worker's env object is passed as the second argument to route handlers. This works but doesn't compose as cleanly with middleware that also needs access to bindings. You end up threading env through more manually, or relying on closure scope, which gets messy in larger codebases.

Cloudflare's official examples and templates now consistently use Hono for Workers APIs. The npm create hono@latest scaffolding generates a cloudflare-workers template with D1, KV, and AI bindings pre-configured, wrangler.toml set up, and a working test setup using vitest-pool-workers. This scaffolding quality gap versus itty-router's minimal documentation is meaningful for teams new to the Workers platform.

For Workers deployments, cold start time is a real concern since Workers boot per-request in some configurations. Both Hono (14KB) and itty-router (0.6KB) are trivially small relative to the V8 isolate overhead, so the bundle size difference doesn't translate to meaningful cold start differences in practice. The Workers platform isolate warm-up dominates.

Migrating from itty-router to Hono

If you have an existing itty-router project and want to migrate to Hono, the route handler signatures are similar enough that migration is usually straightforward. The main conceptual difference is that itty-router passes (request, env, ctx) as positional arguments while Hono uses a single context object c with c.req, c.env, and c.executionCtx. A typical itty-router handler (req, env) => env.KV.get(req.params.key) becomes (c) => c.env.KV.get(c.req.param('key')) in Hono.

itty-router's response helpers (json(), error(), status()) map to Hono's c.json(), c.text(), and c.newResponse(). The AutoRouter's finally: [json] transformer (which auto-wraps plain object returns as JSON) doesn't have a direct equivalent in Hono, but Hono middleware can add a response transformer with similar behavior if you rely on that pattern heavily.

For teams concerned about bundle size after migrating, note that Hono supports tree-shaking — if you only import core routing and skip middleware packages, the bundle footprint is significantly smaller than the full 14KB. A Hono Worker using only basic routing and c.json() compiles to roughly 3-4KB, which is a reasonable tradeoff for the TypeScript and ecosystem benefits.

Compare Hono, itty-router, and Worktop download trends on PkgPulse.

When to Use Each

Use Hono if:

  • You want a full-featured web framework that works across Cloudflare Workers, Bun, Node.js, and Deno
  • You need middleware, JWT auth, validation, and RPC client generation
  • You are building a multi-route API with production-level requirements
  • You want TypeScript support with typed routes and response types
  • Your team needs documentation, examples, and a mature ecosystem

Use itty-router if:

  • Bundle size is critical (e.g., a free-tier Cloudflare Worker with a 1MB limit)
  • You want the absolute minimum routing layer with no overhead
  • Your API has 2-5 routes and doesn't need middleware
  • You need a router for an edge function or service worker environment

Use worktop if:

  • Your Workers project uses the Service Worker API syntax (addEventListener('fetch', ...))
  • You want a router that integrates with the older Cloudflare Workers API style
  • You have an existing worktop codebase you're maintaining

In 2026, Hono is the default recommendation for new Cloudflare Workers projects — it provides the best balance of features, performance, and developer experience across all edge runtimes. itty-router remains a valid choice when bundle size is a hard constraint.

Both Hono and itty-router support the native Request/Response Web API, making them portable across Cloudflare Workers, Deno, Bun, and any standard-compliant edge runtime without code changes.

Hono's RPC client feature (@hono/client) is underutilized but powerful for full-stack TypeScript applications. When you define Hono routes with explicit response types using c.json() and Zod validation, the RPC client can infer the exact response shape for each route — giving you end-to-end type safety from the Cloudflare Worker to the browser client without a code generation step or a separate API schema file. The workflow is: define the Hono app with typed routes, export the app type, and import that type into your frontend client code. hc<AppType>('https://api.example.com') creates a client where client.api.users.$get() returns a typed response inferred directly from your route handler's return type. This is the Workers equivalent of tRPC — no schema, no codegen, just TypeScript types flowing from server to client. For teams building monorepo applications with a Cloudflare Worker backend and a Next.js or React frontend, this pattern eliminates an entire category of frontend-backend type drift bugs.

A Hono behavior that surprises developers coming from Express is route matching precision. Hono's router (internally called RegExpRouter or SmartRouter) matches routes in registration order for the same path pattern, but parameterized segments like :id are always less specific than literal segments. /api/users/me will correctly match before /api/users/:id even if registered after it, because Hono scores specificity. Express resolves /api/users/me as :id = "me" if the parameterized route was registered first. This Hono behavior is correct for REST APIs but requires verifying route order if you have both literal routes and parameterized routes for the same path prefix. In itty-router, route matching is strictly first-match, so registration order is the developer's responsibility.

See also: Fastify vs Hono and Express vs Hono, Hono vs itty-router: Edge-First API Frameworks Compared.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.