Reference

Req & Res

Every handler receives two arguments. req reads the incoming request with params, query, body, headers and Cloudflare bindings. res builds the response with a rendered page, JSON, a redirect or a status code. A handler reads from req, then returns something built with res.

export async function POST(req, res) {
  const { email } = await req.body();
  if (!email) return res.status(400).json({ error: "Email is required" });
  return res.json({ ok: true });
}

A handler must return a Response (or a Promise<Response>). Every res method produces one to return it. Forgetting to return throws a clear framework error.

Route params

Dynamic segments arrive on req.params, already URL-decoded. For src/routes/polls/[id].jsx matching /polls/123:

req.params.id; // "123"

Query strings

req.query() is the friendly reader. It returns a plain object you can destructure. Single values are strings, repeated values are arrays, missing ones are undefined:

export function GET(req, res) {
  const { page, sort } = req.query(); // /posts?page=2&sort=new
  const { tag } = req.query();        // ?tag=react&tag=cloudflare → ["react", "cloudflare"]
  return res.json({ page, sort, tag });
}

For native behavior like ordering, .has(), precise repeated-key control. Reach for req.searchParams, which is the URL's URLSearchParams:

const page = req.searchParams.get("page");
const tags = req.searchParams.getAll("tag");

Reading the body

req.body() is the friendly body reader. You have to await it and it always resolves to a plain object you can safely destructure, so you never need a ?? {} guard.

export async function POST(req, res) {
  const { title } = await req.body();
  // ...
}

It parses application/json and application/x-www-form-urlencoded. Empty bodies, GET/HEAD/OPTIONS requests, multipart, unsupported content types and even malformed JSON all resolve to {}. Repeated form fields become arrays, so normalize a field that can be single or repeated:

const { option } = await req.body();
const options = Array.isArray(option) ? option : option ? [option] : [];

Headers, method and URL

req.headers; // native Headers — req.headers.get("authorization")
req.method;  // "GET", "POST", …
req.url;     // full request URL as a string

Render a page

res.render takes a component reference and props, never JSX. See Rendering.

export async function GET(req, res) {
  const poll = await req.d1.get("SELECT * FROM polls WHERE id = ?", [req.params.id]);
  if (!poll) return res.notFound();
  return res.render(PollPage, { poll }); // not res.render(<PollPage />)
}

JSON

return res.json({ ok: true });
return res.json({ created: true }, 201); // status as the second argument

Redirect

return res.redirect("/thanks");      // 302 by default
return res.redirect("/login", 303);  // or pass a status

Status codes

res.status(code) is chainable and sets the status for the next response method.

return res.status(201).json({ created: true });

A status passed directly to json, text or html overrides a pending status().

Not found

res.notFound() renders your registered _404 page with a 404 status. Use it for resource 404s, a route matched but the record is missing. See Error Pages.

if (!post) return res.notFound();

Going deeper

Strict JSON

req.json() is the strict counterpart to req.body(): use it when an endpoint must receive JSON and invalid input should fail loudly. It returns the parsed value, {} for an empty body, throws 415 for a non-JSON Content-Type and 400 for malformed JSON.

const { email } = await req.json();

Use req.body() for forgiving app handlers, req.json() for strict API endpoints.

Forms and uploads

req.form() is the precise form reader for when repeated values, ordering or file uploads matter. It returns URLSearchParams for urlencoded bodies and FormData for multipart bodies, and throws 415 for anything else.

const form = await req.form();
const title = form.get("title");
const options = form.getAll("option");

Bodies over 10 MiB are rejected with 413 Payload Too Large before they are buffered. For larger uploads, stream from req.request.body or upload directly to R2.

Raw bodies

When the exact bytes matter like Stripe's webhook signature verification, binary payloads, read the body raw:

const raw = await req.text();           // raw body as text
verifySignature(raw, req.headers.get("stripe-signature"));
const bytes = await req.arrayBuffer();  // raw body as bytes

All body readers are lazy and cached: each reads from its own clone, so mixing req.body(), req.text() and the rest in one handler is safe and never re-reads the request.

Cloudflare bindings

req.env is the raw Cloudflare environment. Every binding, var and secret from your Cloudflare configuration lives there.

const key = req.env.STRIPE_KEY;
await req.env.MY_QUEUE.send({ type: "signup" });

Goribu wraps the common ones in helpers. Use those for app code and drop to req.env for the native binding:

req.d1;       // D1 helper — see /docs/d1
req.postgres; // Hyperdrive Postgres helper — see /docs/postgres
req.cache;    // Cache API helper — see /docs/cache

req.request is the underlying native Request, for when a third-party library needs the original.

Other response helpers

res.text(body, status?); // text/plain
res.html(body, status?); // text/html, for ad-hoc HTML strings
res.vary(token);         // declare a header the response varies on; chainable

res.vary accumulates across middleware and chains onto a render:

return res.vary("Cookie").render(Page, props);