Defining routes
Route patterns, path params, body validation, and query strings.
Routes are defined with a single string containing the HTTP method and the full path:
zen.route
"GET /hello/<name>",
p=> `Hello, ${p.name}!`
Route params are enclosed in angle brackets. They are automatically URI-decoded and cannot be empty or optional.
Multiple params work as expected:
zen.route
"GET /api/rooms/<roomId>/users/<userId>",
asyncp=>
// p.roomId and p.userId are both typed strings
All route paths are fully qualified and
greppable. There
are no base prefixes, not on ZenRouter, not on ZenRelay. Every route
definition is always the complete URL path. This is one of the core
principles.
If a request is made to a URL that doesn’t match any route, Zen Router returns
404 Not Found. If a request is made to a path that exists but with a method
that isn’t defined, Zen Router will automatically return the correct 405 Method Not Allowed response.
Route param schema
By default, all path params will get exposed as strings in p. This is a good
default since path params are strings. If you want, however, you can limit
their allowed inputs or convert them to other types (e.g. numbers, enums,
etc.). You can define route param schema at the router level using any
Standard Schema compatible library:
importfrom "zod";
const zen = new ZenRouter
number
enum"red", "green", "blue"
});
zen.route
"GET /api/posts/<postId>/authors/<index>",
// ^^^^^^ ^^^^^
// string number
p=> ...
zen.route
"POST /api/posts/<postId>/tag/<color>",
// ^^^^^^ ^^^^^
// string 'red' | 'green' | 'blue'
p=> ...
If any param fails this validation, a 400 Bad Request will be returned. See
the Request lifecycle. For example, a request to
/api/posts/123/tag/yellow would be rejected because yellow is not a valid
color.
Body validation
To validate a request body, pass a schema as the second argument to .route().
The body is validated before your handler runs, so body is already parsed and
fully typed. If you don’t provide a schema, body is typed as never so
TypeScript won’t let you access it. Accessing it at runtime throws an error.
importfrom "zod";
zen.route
"POST /api/posts",
z.objectstring
asyncauth, body=>
const post = awaitcreatePost
return
Zen Router supports any validation library that implements the Standard Schema spec. For use on Cloudflare Workers, we recommend decoders for its smaller bundle size and better error messages.
If the body doesn’t match the schema, Zen Router returns
422 Unprocessable Entity with a human-readable error message. See the Request lifecycle.
Query strings
Query string parameters are available via q. They are always optional strings.
// GET /api/posts?sort=newest&limit=10
zen.route"GET /api/posts", ({ q=>
// q.sort = "newest"
// q.limit = "10"
// q.other = undefined
});If a query param appears multiple times, only the first value is captured. For
full control, use url.searchParams directly.
Handler arguments
Every route handler receives a single object with the following properties:
| Property | Description |
|---|---|
req | The original, unmodified Request |
url | Parsed URL (equivalent to new URL(req.url)) |
ctx | Value returned by your getContext |
auth | Value returned by your authorize |
p | Typed route params |
q | Query string params |
body | Validated request body (if schema is provided) |
Return values
Handlers can return a JSON object and Zen Router will serialize it with a 200
status automatically. It must be an object, not
an array or scalar. For other response types, see response
helpers.
zen.route
"GET /example",
=>
// Returning a simple object → JSON response
return1, 2, 3