Requests
Route handlers receive a single RequestFacade object that bundles everything
you need to handle an incoming HTTP request: the parsed body, path
parameters, query parameters, cookies, headers, the original WHATWG
Request, and a URL helper. This page walks through each part and shows how
to access and validate it.
| Property | Type | Description |
|---|---|---|
| body | RequestBody | parsed request body |
| path | RequestBag | path parameters |
| query | RequestBag | query parameters |
| headers | RequestBag | request headers |
| cookies | RequestBag | request cookies (case-sensitive) |
| original | Request |
original WHATWG Request object |
| url | URL |
original request URL |
| forward | (to: string) => Promise<Response> |
forward the request |
Request context
RequestFacade includes a request context store: request-scoped values that
hooks can attach for downstream hooks and route handlers to read.
Use request context for derived server values (e.g. authenticated user,
locale, feature flags, request IDs). For client input, use body, path,
query, headers, or cookies.
Context is most commonly set in hooks.
API
| Method | Description |
|---|---|
request.set(key, value) |
Set a context value |
request.set<T>(key, fn) |
Update a context value with a function |
request.get<T>(key) |
Get a context value (throws if missing) |
request.try<T>(key) |
Get a context value or undefined |
request.has(key) |
Check if a context key exists |
request.delete(key) |
Remove a context key |
Request. request.forward()
forwards the original request, not your attached context.Golden path: authenticate in a hook, use in a route
// routes/+hook.ts
import hook from "primate/route/hook";
hook(async (request, next) => {
const user = await authenticate(request);
return next(request.set("auth.user", user));
});
## Body
The parsed request body. Primate decodes the body according to the
`Content-Type` header sent with the request, and `request.body` offers different
methods to retrieve the body. `request.body` enforces the correct accessor for
the incoming `Content-Type` (e.g., `json()` for `application/json`) and throws
on a mismatch.
<div class="tabbed"><span class="captionline">
<span class="captions"><span class='active'> TypeScript</span><span> Go</span></span><span class="filenames"><span class='active'>routes/identify.ts</span><span>routes/identify.go</span></span></span><span class="tabs"><div>
```ts
import p from "pema";
import route from "primate/route";
route.post(request => {
const { name } = request.body.json(p({ name: p.string.min(1) }));
return `Hello, ${name}`;
});package main
import (
"fmt"
"github.com/primate-run/go/route"
)
var _ = route.Post(func(request route.Request) any {
var m map[string]any
if err := request.Body.JSON(&m); err != nil {
return map[string]any{"error": err.Error()}
}
name, _ := m["name"].(string)
return fmt.Sprintf("Hello, %s", name)
})If a client sends a POST request to /identify using the content type
application/json and {"name": "John"} as payload, this route responds with
200 OK and the body Hello, John.
| Content type | Method |
|---|---|
text/plain |
text() |
application/json |
json() |
application/x-www-form-urlencoded |
form() |
multipart/form-data |
form() — values are FormDataEntryValue (string or File) |
application/octet-stream |
binary() |
| no body | none() |
RequestBody reference
import type JSONValue from "@rcompat/type/JSONValue";
interface RequestBody {
type: "text" | "json" | "form" | "binary" | "none";
text(): string;
json(): JSONValue;
json<S>(schema: { parse(x: unknown): S }): S;
form(): Record<string, FormDataEntryValue>;
form<S>(schema: { parse(x: unknown): S }): S;
binary(): Blob;
none(): null;
files(): Record<string, File>;
}
Path
Path parameters extracted from bracketed segments in your route path (e.g.
routes/users/[id], routes/blog/[year]/[slug]).
Path parameters are exposed
as a RequestBag, with get(), try(), has(), and
schema-validated as().
import route from "primate/route";
route.get(request => {
// throws if missing
const id = request.path.get("id"); // "42" for /users/42
return `User #${id}`;
});If a parameter is optional in your route, prefer try() and handle undefined.
import route from "primate/route";
route.get(request => {
const year = request.path.get("year");
const slug = request.path.try("slug"); // string | undefined
return slug ? `Post ${slug} from ${year}` : `All posts in ${year}`;
});[[slug]] to denote an optional
path segment.You can validate/coerce all parameters at once with a schema.
import p from "pema";
import route from "primate/route";
const PathSchema = p({ id: p.string.regex(/^\d+$/) });
route.get(request => {
const { id } = request.path.parse(PathSchema); // id: string (digits only)
return `User #${id}`;
});Query
The query string is split into individual parameters and exposed as a
RequestBag. Use it to read ?page=2&search=john-style parameters. Query
string parameters are matched case-insensitively. If a query parameter appears
multiple times, Primate keeps only the last.
import route from "primate/route";
route.get(request => {
const page = Number(request.query.try("page") ?? "1"); // 1 if missing
const term = request.query.get("search");
return `Searching '${term}' (page ${page})`;
});Schema-validate the entire query string:
import p from "pema";
import route from "primate/route";
const QuerySchema = p({
page: p.int.coerce.min(1).default(1),
search: p.string.min(1),
});
route.get(request => {
const { page, search } = request.query.parse(QuerySchema);
return `Searching '${search}' (page ${page})`;
});request.query normalizes keys, request.url.searchParams does not.Headers
Request headers as a RequestBag. Header keys are matched case-insensitively.
If a header appears multiple times, the last value is kept.
import route from "primate/route";
route.get(request => {
const ua = request.headers.try("user-agent"); // may be undefined
const contentType = request.headers.try("content-type");
// returned as JSON
return { ua, contentType };
});Validate or transform headers with a schema.
import p from "pema";
import Status from "primate/http/Status";
import route from "primate/route";
const HeadersSchema = p({
"content-type": p.string.optional(),
authorization: p.string.startsWith("Bearer ").optional(),
});
route.get(request => {
const headers = request.headers.parse(HeadersSchema);
const token = headers.authorization?.slice("Bearer ".length);
const status = token ? Status.NO_CONTENT : Status.UNAUTHORIZED;
return new Response(null, { status });
});
Cookies
Request cookies as a RequestBag. Cookie names are case-sensitive (unlike
other request bags). If a cookie repeats, Primate keeps the last value.
import Status from "primate/http/Status";
import response from "primate/response";
import route from "primate/route";
route.get(request => {
const session = request.cookies.try("session"); // string | undefined
if (!session) {
return response.error({
body: "Unauthorized",
status: Status.UNAUTHORIZED,
});
}
return `Hello (session ${session.slice(0, 8)}...)`;
});As with other bags, you can parse/validate the whole set.
import p from "pema";
import Status from "primate/http/Status";
import response from "primate/response";
import route from "primate/route";
const CookieSchema = p({
session: p.string.uuid().optional(),
});
route.get(request => {
const { session } = request.cookies.parse(CookieSchema);
return response.text(session ? "OK" : "No session", {
status: session ? Status.OK : Status.FORBIDDEN,
});
});
RequestBag reference
interface RequestBag {
get(key: string): string;
try(key: string): string | undefined;
has(key: string): boolean;
as<T>(schema: { parse(x: unknown): T }): T;
toJSON(): Record<string, string>;
}
Original
The original WHATWG Request object as received from the runtime. Use this for
low-level capabilities like clone(), signal, or direct header access when
needed.
import route from "primate/route";
route.get(request => {
// Abort handling if the client disconnects
request.original.signal.addEventListener("abort", () => {
console.log("client disconnected");
});
// Access a raw header
const lang = request.original.headers.get("Accept-Language");
return new Response(lang ?? "en-US");
});
URL
A URL instance representing the request URL. Handy for composing absolute or
relative URLs, accessing the searchParams directly, etc.
import route from "primate/route";
route.get(request => {
const url = request.url;
// prefer request.query over url.searchParams
const page = url.searchParams.get("page") ?? "1";
// build a new URL relative to the request
const cdn = new URL("/assets/logo.svg", url);
// returned as JSON
return { page, cdn: cdn.href };
});
Forward
Forwards the original WHATWG Request. The method lives on RequestFacade,
but what's forwarded is the underlying original request.
If your handler will pass the request upstream and you don't need to read the
body, disable body parsing on the route. Primate can't know whether you'll call
request.forward() inside the handler, so you must explicitly opt out of
body parsing on the route.
Most other aspects of the original request are preserved. By default only the
Content-Type header is forwarded to match the body — you can specify
additional headers to forward.
import route from "primate/route";
route.get(request => {
return request.forward("https://upstream.internal/service", {
Authorization: request.headers.try("authorization") ?? "",
Accept: request.headers.try("accept") ?? "",
});
}, { parseBody: false });Use this to implement simple reverse proxies, edge routing, or fan-out/fan-in patterns.
RequestFacade reference
import type RequestBag from "@primate/core/request/RequestBag";
import type RequestBody from "@primate/core/request/RequestBody";
interface RequestFacade {
body: RequestBody;
path: RequestBag;
query: RequestBag;
headers: RequestBag;
cookies: RequestBag;
context: Record<string, unknown>;
original: Request;
url: URL;
forward(to: string, headers?: Record<string, string>): Promise<Response>;
}