Requests & Responses

Every Reepolee handler takes a BunRequest and returns a Response. Both types are part of the standard fetch-style API that Bun's HTTP server exposes, so anything you'd do with Request and Response in a worker or edge function works the same way here. This page covers the request-side helpers you'll reach for daily - URL parameters, query strings, and form bodies - and the most common patterns for building responses, including redirects.

URL Parameters

Routes that include :param segments expose those values on req.params. The names match the segment names in the route definition:

"/users/:id/edit": {
    GET: get_users_edit,
    POST: post_users_edit,
},
export async function get_users_edit(req: BunRequest<"/users/:id/edit">): Promise<Response> {
    const ctx = await create_ctx(req, import.meta.dir);
    const id = req.params.id;
    const record = await get_record_by_id(id);
    return render("users/form", { data: { record }, ctx });
}

Values arrive as strings. Coerce them yourself if you need a number:

const id = Number(req.params.id);

Nested routes expose every named segment through the same req.params object:

export async function get_project_task(req: BunRequest<"/projects/:project_id/tasks/:task_id">): Promise<Response> {
    const project_id = req.params.project_id;
    const task_id = req.params.task_id;
    // ...
}

This works for prefixed routes too. There is no need to infer parameter positions from the URL.

Query Strings

Read query parameters directly from the URL:

const url = new URL(req.url);
const query = url.searchParams.get("query") || "";
const offset = parseInt(url.searchParams.get("offset") || "0", 10);

The list view in every generated CRUD module reads query, sort, direction, offset, and limit this way, which is what makes filtered views bookmarkable.

Form Bodies

For POST handlers that receive application/x-www-form-urlencoded (the default for HTML forms without an enctype), read the body as text and parse it with URLSearchParams:

export async function post_users_index(req: BunRequest): Promise<Response> {
    const body = await req.text();
    const params = new URLSearchParams(body);

    const data = {
        email: params.get("email")?.trim() || "",
        name: params.get("name")?.trim() || "",
    };
    // ...
}

For multipart forms (file uploads) use req.formData() - see File Uploads.

JSON Bodies

For endpoints called from JavaScript with Content-Type: application/json, use req.json():

export async function post_users_validate(req: BunRequest): Promise<Response> {
    const body = await req.json();
    const touched: string[] = body.touched || [];
    // ...
}

The live validation endpoint that FormController calls on every blur uses exactly this shape - see Validation.

Building Responses

The render() helper returns a Response with Content-Type: text/html and status 200 by default. To override either, pass them in the options object:

return render("dashboard/index", {
    data: { user },
    status: 200,
    headers: {
        "Cache-Control": "no-store",
        "X-Frame-Options": "DENY",
    },
    ctx,
});

For non-HTML responses, construct the Response directly:

return Response.json({ ok: true });
return new Response("plain text reply", {
    headers: { "Content-Type": "text/plain" },
});

Bun.file() is the cleanest way to stream a file back - set the appropriate Content-Type and Bun handles the rest:

return new Response(Bun.file("/tmp/report.pdf"), {
    headers: { "Content-Type": "application/pdf" },
});

Redirects

Use Response.redirect() with status 303 after a successful write operation. 303 See Other ensures the browser follows the redirect with a GET request regardless of the original method, preventing duplicate submissions on refresh:

return Response.redirect("/users", 303);

If your form included a _return_url hidden field - populated by the client with the URL the user came from - you can redirect back to that page after a save:

const return_url = params.get("_return_url");
const redirect_url = return_url?.includes("/users") ? return_url : "/users";
return Response.redirect(redirect_url, 303);

Always validate the _return_url value before using it. The check above ensures the URL belongs to the same feature and prevents an open-redirect issue.

Redirects That Set Cookies

Response.redirect() does not let you attach extra headers, so redirects that need to set a cookie (like a toast notification or a session cookie after login) are built manually:

const headers = new Headers({ Location: "/users" });
headers.append("Set-Cookie", session_cookie.toString());

return new Response(null, { status: 303, headers });

The body is null, the status is 303, and any number of Set-Cookie headers can be appended.