Request Routing with routeDORequest
The routeDORequest function provides intelligent routing of HTTP and WebSocket requests
to Durable Objects with support for authentication hooks and prefix matching. It's a near
drop-in replacement for Cloudflare agents package routeAgentRequest and PartyKit's
routePartyRequest, but uses standard Cloudflare naming conventions (no "party", "room",
"agent") and has many useful upgrades.
Installation
npm install @lumenize/utils
Basic Usage
Use in a Worker's fetch handler:
async fetch(request: Request, env: Env): Promise<Response> {
// Try routing to a Durable Object
const response = await routeDORequest(request, env);
if (response) return response;
// Fallback for non-DO routes
return new Response('Not Found', { status: 404 });
}
URL Format
Requests must follow this format:
[/${prefix}]/${doBindingName}/${doInstanceNameOrId}[/path...]
Instance ID Handling
The function automatically detects 64-character hex strings (from
newUniqueId().toString()) and uses idFromString() instead of getByName():
-
Named instance
- Path:
/my-do/lobby - Resolves to:
env.MY_DO.getByName('lobby')
- Path:
-
Unique ID (64‑char hex)
- Path:
/my-do/8aa7a69131efa8902661702e701295f168aa5806045ec15d01a2f465bd5f3b99 - Resolves to:
env.MY_DO.get(env.MY_DO.idFromString('8aa7...'))
- Path:
-
Named instance with prefix
- Path:
/do/user-do/john - With config:
{ prefix: '/do' } - Resolves to:
env.USER_DO.getByName('john')
- Path:
Supported Case Conversions for doBindingName
The function intelligently converts URL path segments to match various binding naming conventions, but only for kebab-case URLs (kebab-case-with-only-lowercase-and-digits-3-42). Any path segment containing uppercase letters, underscores, or other characters will match exactly as-is.
Smart matching (kebab-case only):
| URL Path Segment | Matches Binding Names |
|---|---|
/my-do/... | MY_DO, MyDO, MyDo, myDo, my-do |
/user-session/... | USER_SESSION, UserSession, userSession, user-session |
/my-d-o/... | MY_D_O, MyDO, myDO, my-d-o |
/api-v2/... | API_V2, ApiV2, apiV2, api-v2 |
Exact matching only (non-kebab-case):
| URL Path Segment | Matches Binding Names |
|---|---|
/MY_DO/... | MY_DO only |
/MyDO/... | MyDO only |
/my_do/... | my_do only |
Hooks
onBeforeRequest
onBeforeRequest runs for HTTP requests before they reach your Durable Object. Use it to:
- reject requests early by returning a
Response(e.g., 401/403), - modify the
Request(return a newRequest), or - continue unchanged by returning
undefined.
This hook does not run for WebSocket upgrade requests.
onBeforeConnect
onBeforeConnect runs only for WebSocket upgrade requests (Upgrade: websocket). Use it to validate or enrich the request before the upgrade. Return a Response to block, a modified Request to continue with changes, or undefined to proceed as-is.
Examples
Use hooks to validate or modify requests before they reach your Durable Objects:
const response = await routeDORequest(request, env, {
// Hook for WebSocket connections
onBeforeConnect: async (request, { doNamespace, doInstanceNameOrId }) => {
const token = request.headers.get('Authorization');
if (!token || !await validateToken(token)) {
return new Response('Unauthorized', { status: 401 });
}
// Add user info to headers
const modifiedRequest = new Request(request);
modifiedRequest.headers.set('X-User-ID', await getUserId(token));
return modifiedRequest;
},
// Hook for HTTP requests
onBeforeRequest: async (request, { doNamespace, doInstanceNameOrId }) => {
const apiKey = request.headers.get('X-API-Key');
if (request.method !== 'GET' && !apiKey) {
return Response.json(
{ error: 'API key required' },
{ status: 403 }
);
}
}
});
Prefix Matching
Use the prefix option to scope routing to a specific URL path:
const response = await routeDORequest(request, env, { prefix: '/do' });
if (response) return response;
routeDORequest will short-circuit return undefined if the Request.url doesn't match the
prefix.
Lumenize's own routeAgentRequest
@lumenize/utils also exports our own routeAgentRequest. It defaults the prefix to
'/agents' and adds the same x-partykit-... headers that agents routeAgentRequest
does. This is the only place in Lumenize, where we allowed PartyKit foo to creep in and
we only did it so it could serve as a near drop in replacement with much better CORS
support.
It's still only "near" drop-in. Lumenize's onBeforeConnect and onBeforeRequest hooks
don't support some of the optional parameters as agents' because they too have
PartyKit foo in them.
CORS Support
The most common cross-origin resource sharing (CORS) configuration and our recommendation 90% of the time is a simple allowlist. This is not supported by agents'
routeAgentRequest.
// Allowlist specific origins
await routeDORequest(request, env, {
cors: {
origin: ['https://app.example.com', 'https://admin.example.com']
}
});
The following examples match the behavior of agents' routeAgentRequest using the same
configuration but we recommend against their use particularly if your DOs use WebSockets
with a browser because the browser does not natively provide CORS protection for
WebSocket use.
await routeDORequest(request, env, { cors: true }); // Allow all origins (permissive)
await routeDORequest(request, env ); // Default, only allow `fetch` on this origin
// Note, the default ('false') allows WebSocket access for ALL origins
CORS support goes beyond true, false, and a allowlist. For a detailed discussion
and all available options in case you have an advanced use case see
CORS Support.
When enabled, CORS support automatically:
- Reflects the allowed origin in
Access-Control-Allow-Originheader - Sets
Vary: Originfor proper caching - Handles OPTIONS (preflight) requests
- Adds headers to both DO responses and
onBefore...hook responses
Multiple Routers
When using multiple routing functions (e.g., routeDORequest and routeAgentRequest),
use the idiomatic pattern with || for clean fallthrough. Each router checks its prefix
and returns undefined if it doesn't match:
import { routeDORequest, routeAgentRequest } from '@lumenize/utils';
// ...
async fetch(request: Request, env: Env): Promise<Response> {
return (
await routeDORequest(request, env, { prefix: '/do' }) ||
await routeAgentRequest(request, env) || // default prefix is '/agents'
new Response("Not Found", { status: 404 })
);
}
Error Handling
The function throws errors for invalid requests:
MissingInstanceNameError(400) - Binding name found but instance name/ID missingMultipleBindingsFoundError(400) - Multiple DO bindings match the path segment (configuration error)
Note, if the url segment for doBindingName doesn't match any DO Namespaces found on
env, it will return undefined, not an error.
Each error includes an httpErrorCode property in addition to the standard message
property for easy HTTP response handling:
import { routeDORequest, routeAgentRequest } from '@lumenize/utils';
// ...
async fetch(request: Request, env: Env): Promise<Response> {
try {
return (
await routeDORequest(request, env, { prefix: '/do' }) ||
await routeAgentRequest(request, env) || // default prefix is '/agents'
new Response("Not Found", { status: 404 })
);
} catch (error: any) {
// Handle MissingInstanceNameError, MultipleBindingsFoundError, etc.
const status = error.httpErrorCode || 500;
return new Response(error.message, { status });
}
}
Key Features
- Case-insensitive binding name matching - Flexible naming conventions
- Automatic ID detection - Handles both named instances and unique IDs
- Hooks - Authenticate, validate, or enhance requests before they reach DOs
- Prefix support - Scope routing to specific URL paths
- Hono-style behavior - Returns
undefinedif request doesn't match - Standard Cloudflare naming - No confusing party/agent/room terminology
- Robust CORS support - allowlist and custom function in addition to true/false