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) |
context | Record<string, unknown> |
initial context for the client |
original | Request |
original WHATWG Request object |
url | URL |
original request URL |
forward | (to: string) => Promise<Response> |
forward the request |
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.
import pema from "pema";
import string from "pema/string";
import route from "primate/route";
route.post(request => {
const { name } = request.body.json(pema({ name: 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 |
fields() |
multipart/form-data |
fields() — 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" | "fields" | "binary" | "none";
text(): string;
json(): JSONValue;
json<S>(schema: { parse(x: unknown): S }): S;
fields(): Record<string, FormDataEntryValue>;
fields<S>(schema: { parse(x: unknown): S }): S;
binary(): Blob;
none(): null;
}
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 pema from "pema";
import string from "pema/string";
import route from "primate/route";
const Path = pema({ id: string.regex(/^\d+$/) });
route.get(request => {
const { id } = request.path.parse(Path); // 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 pema from "pema";
import int from "pema/int";
import string from "pema/string";
import route from "primate/route";
const Query = pema({
page: int.coerce.min(1).default(1),
search: string.min(1),
});
route.get(request => {
const { page, search } = request.query.parse(Query);
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 pema from "pema";
import string from "pema/string";
import Status from "primate/response/Status";
import route from "primate/route";
const Header = pema({
"content-type": string.optional(),
authorization: string.startsWith("Bearer ").optional(),
});
route.get(request => {
const headers = request.headers.parse(Header);
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 response from "primate/response";
import Status from "primate/response/Status";
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 pema from "pema";
import string from "pema/string";
import response from "primate/response";
import Status from "primate/response/Status";
import route from "primate/route";
const Cookie = pema({
session: string.uuid().optional(),
});
route.get(request => {
const { session } = request.cookies.parse(Cookie);
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>;
}
Context
Initial context for the client. This is a plain dictionary available during route execution and intended for values you want to expose to the client on initial load.
import response from "primate/response";
import route from "primate/route";
// Add data to the initial client context
route.get(request => {
request.context.greeting = "Welcome!";
request.context.env = "production";
return response.view("Hello.jsx");
});
Use context
as a small, serializable key–value store for data you want on
initial render.
props.request.context
. To avoid polluting
the props object, this API may change in the future.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>;
}