Frameworks Integration
Use when asking about "Hono", "itty-router", "routing frameworks", "middleware", "API framework", "web framework for Workers", "request routing", "Express-like", or choosing between web frameworks for Cloudflare Workers.
SKILL.md
Full skill instructions
Web Frameworks for Workers
Purpose
This skill provides guidance on web frameworks for Cloudflare Workers, with focus on Hono and itty-router—the two most popular choices. Use this when building API endpoints, adding middleware, or choosing between vanilla fetch handlers and framework-based routing.
When to Use a Framework
Use a framework when:
- Multiple routes/endpoints (>3-4)
- Need middleware (auth, logging, CORS)
- Building a REST API
- Want type-safe routing
- Team is familiar with Express-like patterns
Stay vanilla when:
- Single endpoint or simple proxy
- Maximum performance critical
- Minimal dependencies desired
- Learning Workers fundamentals
Framework Comparison
| Feature | Hono | itty-router | Vanilla |
|---|---|---|---|
| Bundle size | ~14KB | ~1KB | 0KB |
| Type safety | Excellent | Good | Manual |
| Middleware | Built-in system | Basic | Manual |
| Learning curve | Medium | Low | Low |
| Documentation | Extensive | Moderate | N/A |
| Active development | Very active | Active | N/A |
| Best for | Full APIs | Simple routing | Single endpoint |
Hono
Why Hono
- Express-like API, easy to learn
- Excellent TypeScript support
- Rich middleware ecosystem
- Built for edge runtimes
- Active community and development
Basic Setup
import { Hono } from 'hono';
type Bindings = {
DATABASE: D1Database;
AI: Ai;
CACHE: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
// Routes
app.get('/', (c) => c.text('Hello from Hono!'));
app.get('/api/users', async (c) => {
const users = await c.env.DATABASE
.prepare('SELECT * FROM users')
.all();
return c.json(users.results);
});
app.post('/api/users', async (c) => {
const body = await c.req.json();
// Validate and create user
return c.json({ id: '123', ...body }, 201);
});
export default app;
Middleware
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { bearerAuth } from 'hono/bearer-auth';
const app = new Hono<{ Bindings: Bindings }>();
// Global middleware
app.use('*', logger());
app.use('*', cors({
origin: ['https://example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
// Route-specific middleware
app.use('/api/*', bearerAuth({ token: 'secret' }));
// Or custom middleware
app.use('/api/*', async (c, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
c.header('X-Response-Time', `${ms}ms`);
});
Route Groups
const app = new Hono<{ Bindings: Bindings }>();
// API routes
const api = new Hono<{ Bindings: Bindings }>();
api.get('/users', usersHandler);
api.get('/users/:id', userByIdHandler);
api.post('/users', createUserHandler);
// Mount the group
app.route('/api', api);
// Or inline grouping
app.route('/api/v2', new Hono()
.get('/health', (c) => c.json({ status: 'ok' }))
.get('/version', (c) => c.json({ version: '2.0' }))
);
Error Handling
import { HTTPException } from 'hono/http-exception';
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status);
}
console.error('Unhandled error:', err);
return c.json({ error: 'Internal server error' }, 500);
});
// Throwing HTTP exceptions
app.get('/api/users/:id', async (c) => {
const id = c.req.param('id');
const user = await getUser(id);
if (!user) {
throw new HTTPException(404, { message: 'User not found' });
}
return c.json(user);
});
With Workers AI
app.post('/api/chat', async (c) => {
const { message } = await c.req.json();
const response = await c.env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: message }
]
});
return c.json({ response: response.response });
});
// Streaming response
app.post('/api/chat/stream', async (c) => {
const { message } = await c.req.json();
const stream = await c.env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
messages: [{ role: 'user', content: message }],
stream: true
});
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' }
});
});
Validation with Zod
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().positive().optional(),
});
app.post('/api/users',
zValidator('json', createUserSchema),
async (c) => {
const user = c.req.valid('json');
// user is typed and validated
return c.json({ id: '123', ...user }, 201);
}
);
itty-router
Why itty-router
- Extremely lightweight (~1KB)
- Dead simple API
- No build step required
- Good for simple APIs
- Easy to understand source code
Basic Setup
import { Router } from 'itty-router';
interface Env {
DATABASE: D1Database;
}
const router = Router();
router.get('/', () => new Response('Hello!'));
router.get('/api/users', async (request, env: Env) => {
const users = await env.DATABASE
.prepare('SELECT * FROM users')
.all();
return Response.json(users.results);
});
router.post('/api/users', async (request, env: Env) => {
const body = await request.json();
// Create user
return Response.json({ id: '123', ...body }, { status: 201 });
});
// 404 fallback
router.all('*', () => new Response('Not Found', { status: 404 }));
export default {
fetch: (request: Request, env: Env, ctx: ExecutionContext) =>
router.handle(request, env, ctx)
};
With Middleware Pattern
import { Router } from 'itty-router';
const router = Router();
// Simple auth middleware
const withAuth = async (request: Request, env: Env) => {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token || token !== env.API_KEY) {
return new Response('Unauthorized', { status: 401 });
}
// Don't return anything to continue to next handler
};
// Apply middleware to routes
router.get('/api/*', withAuth);
router.get('/api/data', (request, env) => Response.json({ data: 'secret' }));
Route Parameters
router.get('/api/users/:id', async (request, env) => {
const { id } = request.params;
const user = await env.DATABASE
.prepare('SELECT * FROM users WHERE id = ?')
.bind(id)
.first();
if (!user) {
return new Response('Not Found', { status: 404 });
}
return Response.json(user);
});
// Optional parameters
router.get('/api/posts/:id?', (request) => {
const { id } = request.params;
if (id) {
return Response.json({ post: id });
}
return Response.json({ posts: [] });
});
Choosing Between Them
Choose Hono When
- Building a substantial API (>10 endpoints)
- Need built-in middleware (CORS, auth, validation)
- Want excellent TypeScript integration
- Team comes from Express/Fastify background
- Building something that will grow
Choose itty-router When
- Simple API with few endpoints
- Bundle size is critical
- Want minimal abstraction
- Quick prototype or proof of concept
- Learning Workers fundamentals
Choose Vanilla When
- Single-purpose Worker (proxy, redirect, etc.)
- Maximum control needed
- Zero dependencies required
- Edge case the frameworks don't handle well
Vanilla Fetch Handler Pattern
For comparison, here's the vanilla approach:
interface Env {
DATABASE: D1Database;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
const method = request.method;
// Manual routing
if (path === '/' && method === 'GET') {
return new Response('Hello!');
}
if (path === '/api/users' && method === 'GET') {
const users = await env.DATABASE
.prepare('SELECT * FROM users')
.all();
return Response.json(users.results);
}
if (path.match(/^\/api\/users\/[\w-]+$/) && method === 'GET') {
const id = path.split('/').pop();
const user = await env.DATABASE
.prepare('SELECT * FROM users WHERE id = ?')
.bind(id)
.first();
if (!user) {
return new Response('Not Found', { status: 404 });
}
return Response.json(user);
}
return new Response('Not Found', { status: 404 });
}
};
Migration Patterns
From Vanilla to Hono
// Before (vanilla)
export default {
async fetch(request: Request, env: Env) {
if (request.method === 'GET' && new URL(request.url).pathname === '/') {
return new Response('Hello');
}
return new Response('Not Found', { status: 404 });
}
};
// After (Hono)
import { Hono } from 'hono';
const app = new Hono<{ Bindings: Env }>();
app.get('/', (c) => c.text('Hello'));
export default app;
From itty-router to Hono
// Before (itty-router)
import { Router } from 'itty-router';
const router = Router();
router.get('/api/users/:id', (req, env) => {
const { id } = req.params;
return Response.json({ id });
});
// After (Hono)
import { Hono } from 'hono';
const app = new Hono<{ Bindings: Env }>();
app.get('/api/users/:id', (c) => {
const id = c.req.param('id');
return c.json({ id });
});
Best Practices
Type Safety
Always type your bindings:
type Bindings = {
DATABASE: D1Database;
CACHE: KVNamespace;
AI: Ai;
};
const app = new Hono<{ Bindings: Bindings }>();
Error Handling
Always have a global error handler:
app.onError((err, c) => {
console.error(err);
return c.json({ error: 'Something went wrong' }, 500);
});
CORS
Configure CORS appropriately:
app.use('*', cors({
origin: process.env.NODE_ENV === 'production'
? ['https://yourdomain.com']
: ['http://localhost:3000'],
}));
Logging
Add request logging in development:
app.use('*', logger()); // Hono's built-in logger
Additional Resources
- Hono documentation: https://hono.dev/
- itty-router: https://github.com/kwhitley/itty-router
- Use cloudflare-docs-specialist for Cloudflare-specific framework guidance
