Workers Routing
Request routing in Cloudflare Workers including URL pattern matching, path parameters, middleware patterns, error handling, CORS configuration, custom domains, route priorities, and Workers for Platforms.
You are an expert in routing and request handling patterns for Cloudflare Workers, including URL matching, middleware composition, error handling, and multi-tenant routing with Workers for Platforms.
## Key Points
1. More specific hostnames beat wildcards.
2. Longer path prefixes beat shorter ones.
3. If two routes match equally, the first one defined wins.
4. A route with a zone_name beats one without.
## Quick Example
```toml
# Specific routes — more specific patterns take priority
routes = [
{ pattern = "api.example.com/v1/*", zone_name = "example.com" },
{ pattern = "example.com/api/*", zone_name = "example.com" },
]
```
```toml
# Automatically provisions DNS and TLS
[[custom_domains]]
hostname = "api.example.com"
```skilldb get cloudflare-workers-skills/Workers RoutingFull skill: 436 linesWorkers Routing — Cloudflare Workers
You are an expert in routing and request handling patterns for Cloudflare Workers, including URL matching, middleware composition, error handling, and multi-tenant routing with Workers for Platforms.
Core Philosophy
Overview
Cloudflare Workers receive a Request object and must return a Response. There is no built-in router — you construct routing logic from URL parsing, pattern matching, and middleware composition. This gives full control over how requests are dispatched and processed.
Basic Routing
URL-based routing
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const { pathname } = url;
const method = request.method;
// Static routes
if (method === "GET" && pathname === "/") {
return new Response("Home");
}
if (method === "GET" && pathname === "/api/health") {
return Response.json({ status: "ok" });
}
if (method === "POST" && pathname === "/api/users") {
return handleCreateUser(request, env);
}
return new Response("Not Found", { status: 404 });
},
};
URLPattern API
Workers support the standard URLPattern API for declarative route matching:
const routes = [
{
pattern: new URLPattern({ pathname: "/api/users/:id" }),
handler: handleGetUser,
},
{
pattern: new URLPattern({ pathname: "/api/users/:userId/posts/:postId" }),
handler: handleGetUserPost,
},
{
pattern: new URLPattern({ pathname: "/api/products/:slug" }),
handler: handleGetProduct,
},
];
export default {
async fetch(request: Request, env: Env): Promise<Response> {
for (const route of routes) {
const match = route.pattern.exec(request.url);
if (match) {
const params = match.pathname.groups;
return route.handler(request, env, params);
}
}
return new Response("Not Found", { status: 404 });
},
};
async function handleGetUser(
request: Request,
env: Env,
params: Record<string, string>
): Promise<Response> {
const userId = params.id;
// Fetch user by ID...
return Response.json({ id: userId, name: "Alice" });
}
Method-aware router
type Handler = (req: Request, env: Env, params: Record<string, string>) => Promise<Response>;
interface Route {
method: string;
pattern: URLPattern;
handler: Handler;
}
function createRouter() {
const routes: Route[] = [];
const router = {
get(path: string, handler: Handler) {
routes.push({ method: "GET", pattern: new URLPattern({ pathname: path }), handler });
return router;
},
post(path: string, handler: Handler) {
routes.push({ method: "POST", pattern: new URLPattern({ pathname: path }), handler });
return router;
},
put(path: string, handler: Handler) {
routes.push({ method: "PUT", pattern: new URLPattern({ pathname: path }), handler });
return router;
},
delete(path: string, handler: Handler) {
routes.push({ method: "DELETE", pattern: new URLPattern({ pathname: path }), handler });
return router;
},
async handle(request: Request, env: Env): Promise<Response> {
for (const route of routes) {
if (request.method !== route.method) continue;
const match = route.pattern.exec(request.url);
if (match) {
return route.handler(request, env, match.pathname.groups as Record<string, string>);
}
}
return new Response("Not Found", { status: 404 });
},
};
return router;
}
const router = createRouter()
.get("/api/users", listUsers)
.get("/api/users/:id", getUser)
.post("/api/users", createUser)
.put("/api/users/:id", updateUser)
.delete("/api/users/:id", deleteUser);
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return router.handle(request, env);
},
};
Middleware Patterns
Composable middleware
type Middleware = (
request: Request,
env: Env,
next: () => Promise<Response>
) => Promise<Response>;
function compose(...middlewares: Middleware[]) {
return async (request: Request, env: Env, finalHandler: () => Promise<Response>) => {
let index = -1;
async function dispatch(i: number): Promise<Response> {
if (i <= index) throw new Error("next() called multiple times");
index = i;
if (i === middlewares.length) {
return finalHandler();
}
return middlewares[i](request, env, () => dispatch(i + 1));
}
return dispatch(0);
};
}
Authentication middleware
const authMiddleware: Middleware = async (request, env, next) => {
const token = request.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const payload = await verifyJWT(token, env.JWT_SECRET);
// Attach user info via a header (Workers cannot mutate request objects directly)
const headers = new Headers(request.headers);
headers.set("X-User-Id", payload.sub);
const authedRequest = new Request(request, { headers });
return next();
} catch {
return Response.json({ error: "Invalid token" }, { status: 403 });
}
};
Logging middleware
const loggingMiddleware: Middleware = async (request, env, next) => {
const start = Date.now();
const response = await next();
const duration = Date.now() - start;
console.log(
JSON.stringify({
method: request.method,
url: request.url,
status: response.status,
duration_ms: duration,
})
);
return response;
};
Error Handling
Global error boundary
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
try {
return await router.handle(request, env);
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
console.error("Unhandled error:", message);
if (env.ENVIRONMENT === "development") {
return Response.json(
{ error: message, stack: err instanceof Error ? err.stack : undefined },
{ status: 500 }
);
}
return Response.json({ error: "Internal Server Error" }, { status: 500 });
}
},
};
Custom error classes
class HTTPError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
class NotFoundError extends HTTPError {
constructor(resource: string) {
super(404, `${resource} not found`);
}
}
class ValidationError extends HTTPError {
constructor(public errors: Record<string, string>) {
super(422, "Validation failed");
}
}
// In the error boundary:
if (err instanceof ValidationError) {
return Response.json({ error: err.message, details: err.errors }, { status: 422 });
}
if (err instanceof HTTPError) {
return Response.json({ error: err.message }, { status: err.status });
}
CORS
CORS headers helper
function corsHeaders(origin: string, methods = "GET, POST, PUT, DELETE, OPTIONS"): Headers {
const headers = new Headers();
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Access-Control-Allow-Methods", methods);
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
headers.set("Access-Control-Max-Age", "86400");
return headers;
}
function handleCORS(request: Request, env: Env): Response | null {
const origin = request.headers.get("Origin") || "";
const allowedOrigins = ["https://app.example.com", "https://admin.example.com"];
if (request.method === "OPTIONS") {
if (allowedOrigins.includes(origin)) {
return new Response(null, { status: 204, headers: corsHeaders(origin) });
}
return new Response(null, { status: 403 });
}
return null; // Not a preflight, continue to handler
}
// Add CORS headers to every response
function withCORS(response: Response, origin: string): Response {
const newResponse = new Response(response.body, response);
newResponse.headers.set("Access-Control-Allow-Origin", origin);
return newResponse;
}
Route Configuration in wrangler.toml
Route patterns and priorities
# Specific routes — more specific patterns take priority
routes = [
{ pattern = "api.example.com/v1/*", zone_name = "example.com" },
{ pattern = "example.com/api/*", zone_name = "example.com" },
]
Route priority rules:
- More specific hostnames beat wildcards.
- Longer path prefixes beat shorter ones.
- If two routes match equally, the first one defined wins.
- A route with a zone_name beats one without.
Custom domains (simpler alternative)
# Automatically provisions DNS and TLS
[[custom_domains]]
hostname = "api.example.com"
Workers for Platforms
Workers for Platforms enables multi-tenant architectures where end users deploy their own Workers on your platform.
Dispatch namespace
[[dispatch_namespaces]]
binding = "DISPATCHER"
namespace = "my-platform"
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Extract tenant from subdomain: tenant1.platform.com
const tenant = url.hostname.split(".")[0];
try {
// Dispatch to the tenant's Worker
const worker = env.DISPATCHER.get(tenant);
return await worker.fetch(request);
} catch (err) {
return Response.json({ error: "Tenant worker not found" }, { status: 404 });
}
},
};
Outbound Workers (intercept tenant subrequests)
Outbound Workers let the platform intercept and modify subrequests made by tenant Workers — useful for injecting authentication, enforcing rate limits, or rewriting URLs.
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Tenant's Worker made a fetch() — intercept it here
const url = new URL(request.url);
// Block requests to internal services
if (url.hostname.endsWith(".internal.example.com")) {
return new Response("Forbidden", { status: 403 });
}
// Add platform-level auth
const headers = new Headers(request.headers);
headers.set("X-Platform-Key", env.PLATFORM_KEY);
return fetch(new Request(request, { headers }));
},
};
Using Hono (Popular Router Framework)
For larger applications, the Hono framework is the community standard:
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/middleware";
type Bindings = {
DB: D1Database;
KV: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
app.use("*", logger());
app.use("/api/*", cors({ origin: "https://app.example.com" }));
app.get("/api/users", async (c) => {
const { results } = await c.env.DB.prepare("SELECT * FROM users").all();
return c.json(results);
});
app.get("/api/users/:id", async (c) => {
const id = c.req.param("id");
const user = await c.env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(id).first();
if (!user) return c.json({ error: "Not found" }, 404);
return c.json(user);
});
app.onError((err, c) => {
console.error(err);
return c.json({ error: "Internal Server Error" }, 500);
});
export default app;
Install this skill directly: skilldb add cloudflare-workers-skills
Related Skills
Durable Objects
Cloudflare Durable Objects for stateful edge computing, covering constructor patterns, storage API, WebSocket support, alarm handlers, consistency guarantees, and use cases like rate limiting, collaboration, and game state.
Workers AI
Cloudflare Workers AI for running inference at the edge, covering supported models, text generation, embeddings, image generation, speech-to-text, AI bindings, and streaming responses.
Workers D1
Cloudflare D1 serverless SQLite database for Workers, covering schema management, migrations, queries, prepared statements, batch operations, local development, replication, backups, and performance optimization.
Workers Fundamentals
Cloudflare Workers runtime fundamentals including V8 isolates, wrangler CLI, project setup, local development, deployment, environment variables, secrets, and compatibility dates.
Workers KV
Cloudflare Workers KV namespace for globally distributed key-value storage, including read/write patterns, caching strategies, TTL, list operations, metadata, bulk operations, and the eventual consistency model.
Workers Patterns
Production patterns for Cloudflare Workers including queue consumers, cron triggers, email workers, browser rendering, Hyperdrive database connection pooling, Vectorize vector search, and the analytics engine.