Request handlers
Read the request, do server work, and return the response the user should receive.
HTTP methods
Export GET, POST, PUT, PATCH, DELETE, HEAD, or OPTIONS from a route. Handlers receive (req, res) and may be async.
// Return JSON without defining a page component.
export function GET(req, res) {
return res.json({ name: req.query.get("name") ?? "friend" });
}Return a res.* result, not a raw Response, JSX, or a plain object. A default React export adds a page; res.render(props) renders that page with serializable data.
Reading the request
| API | Use |
|---|---|
req.method, req.path | HTTP method and URL pathname. |
req.params | Dynamic path segments as strings. |
req.query | URLSearchParams; use get() or getAll(). |
req.headers | The request's Headers. |
req.env | Worker bindings and server environment values. |
req.cookies | Read and queue cookies. |
req.d1, req.kv | Request-scoped data APIs. |
Reading the body
Always await req.body(). Its result depends on the content type:
| Content type | Result |
|---|---|
application/json | A JSON object; arrays and primitive roots are rejected. |
application/x-www-form-urlencoded | An object of fields; repeated names become arrays. |
multipart/form-data | FormData, including uploaded files. |
text/* | A string. |
| Empty or unsupported type | An empty object. |
Handle both FormData and object fields when accepting native and hydrated forms:
// Validate the value regardless of the form's encoding.
export async function POST(req, res) {
const body = await req.body();
const name = body instanceof FormData
? body.get("name")
: typeof body === "object" ? body.name : undefined;
if (typeof name !== "string" || !name.trim()) {
return res.invalid({ name: "Enter your name." });
}
return res.json({ name: name.trim() });
}Malformed JSON or multipart data produces a safe 400 response. await req.rawBody() gives you a Uint8Array for webhook signatures or custom parsing. Both helpers share one body read, so either can be called first.
Choosing a response
| Return | Result |
|---|---|
res.render(props) | Render this route's default page. |
res.json(data) | Return JSON. |
res.text(body) | Return plain text. |
res.invalid(errors) | Return form validation errors, normally with status 422. |
res.redirect(path) | Redirect, normally with status 303. |
res.notFound(props) | Render the app's 404 page. |
Use res.status(code) and res.headers(object) before a terminal helper:
return res.status(201).headers({ "x-created": "yes" }).json({ id: 7 });Bodyless statuses such as 204 cannot carry JSON or text. There is no res.html(); render a page to return HTML.
Redirects
After saving a form, return res.redirect("/posts"). The default 303 tells a native browser submission to follow with GET. Hydrated Forms navigate without repeating the mutation.
For a permanent redirect, set the status explicitly:
// Move an old URL to its replacement.
export function GET(req, res) {
return res.status(301).redirect("/");
}Cookies
Cookie changes are added to the final response, including redirects. Options are explicit; Goribu does not add security defaults.
// Count visits using a cookie scoped to the whole app.
export function GET(req, res) {
const visits = Number(req.cookies.get("visits") ?? "0") + 1;
req.cookies.set("visits", String(visits), {
path: "/",
maxAge: 86400,
httpOnly: true,
sameSite: "Lax",
});
return res.json({ visits });
}get(name) returns a string or null. set() accepts path, domain, expires, maxAge, httpOnly, secure, and sameSite. Encode structured or Unicode values before storing them.
Delete with req.cookies.delete("visits", { path: "/" }), using the same path and domain as the original cookie. A cookie value supplied by the browser is input to validate, not proof of identity.
Middleware
A default export in _middleware.js runs before routes in its folder and subfolders. Parent folders run first. Return nothing to continue or a res.* result to stop; there is no next().
// Add a diagnostic header, then continue to the route.
export default function requestId(req, res) {
res.headers({ "x-request-id": crypto.randomUUID() });
}A middleware file can also default-export an array of functions; those run left to right. Middleware and the handler share the same request and response builder.
Keep middleware scoped to the routes that need it. It makes affected pages Worker-served. It does not run for unmatched URLs or automatic OPTIONS and 405 responses.
Server-only work
Keep private helpers in .server.js or .server.ts files outside the routes folder. Import them from handlers or middleware, never from browser-reachable components. Values passed to res.render() also reach the browser.
Use req.waitUntil(promise) for background work that may finish after the response. Await writes that must succeed before telling the user their change was saved.