Custom Handlers
When a template isn't enough, give the page a handler: sandboxed JavaScript with full control of the request/response cycle. Runs in an isolated Dynamic Worker.
Basics
The handler code is the body of an async function receiving a ctx object. It must return a Response. Handlers respond to GET, POST, PUT, PATCH, and DELETE — branch on ctx.method. Pages without a handler only render templates on GET/POST.
ctx object
| Property | Description |
|---|---|
ctx.method | HTTP method |
ctx.url | Full request URL |
ctx.params | URL path params (/posts/:id → ctx.params.id) |
ctx.query | Query string params |
ctx.body | Parsed request body (after validation) |
ctx.headers | Request headers |
ctx.page | The matched page record |
ctx methods
| Method | Description |
|---|---|
ctx.db.query(sql, params?) | SQL query → { rows, meta } (data statements only) |
ctx.db.batch(statements) | Multiple queries in one round-trip |
ctx.render(template, data) | Render a template string with the template engine |
ctx.sendEmail(opts) | Send transactional email |
ctx.trigger(event, data) | Fire a custom webhook event |
ctx.html(json/status) ctx.json ctx.text | Typed responses |
ctx.redirect(url, status?) | Redirect response (default 302) |
ctx.fetch(input, init?) | Outbound HTTP requests |
Examples
JSON API endpoint
Page path /api/posts/:id:
const post = await ctx.db.query(
"SELECT * FROM __posts WHERE id = ?",
[ctx.params.id]
);
if (post.rows.length === 0) return ctx.json({ error: "Not found" }, 404);
return ctx.json(post.rows[0]);
Send email on form submission
await ctx.sendEmail({
to: "admin@example.com",
subject: "New contact form submission",
html: `<p>Name: ${ctx.body.name}</p><p>Message: ${ctx.body.message}</p>`
});
return ctx.redirect("/thank-you");
Render a template inside a handler
const product = await ctx.db.query(
"SELECT * FROM __products WHERE id = ?", [ctx.params.id]);
const html = ctx.render(
`<h1>{{ product.name }}</h1>`,
{ product: product.rows[0] }
);
return ctx.html(html);
Page validation
Pages can define validation rules that run before rendering and handlers. The validation field takes params, query, and body arrays using the same field definition format:
{
"params": [{ "name": "id", "type": "uuid", "required": true }],
"query": [{ "name": "tab", "type": "string", "enum": ["details", "reviews"] }],
"body": [{ "name": "rating", "type": "number", "min": 1, "max": 5 }]
}
Failed validation returns 404 — intentionally, to avoid revealing that a route exists.
How it works
- Validation rules run first — the handler only fires if they pass.
- The code is hashed (SHA-256); the hash keys the Worker Loader isolate cache. Editing the code spins up a fresh isolate.
- DB, email, and render calls are proxied to the parent worker via RPC — the handler sandbox has no direct binding access.