Route API

primate/route defines HTTP method handlers, validation schemas, and route hooks. In browser builds, the same route export becomes a typed client for its server endpoint.

import route from "primate/route";

The supporting names on this page describe the inferred public contract; they are not additional named exports from primate/route. See the Routing guide for filesystem conventions and walkthroughs.

route()

Normalizes a map of HTTP method handlers for a route file.

function route<R extends RouteHandlers>(handlers: R): Routes<R>;

In a browser build, the same call returns the matching endpoint client:

function route<R extends RouteHandlers>(handlers: R): ClientRoute<R>;

Parameters

Returns: A Routes<R> object retaining each method's handler, options, and inferred page-props type. Primate consumes this object when it builds the filesystem router.

// routes/user/[id].ts
import route from "primate/route";

export default route({
  get(request) {
    return `User ${request.path.get("id")}`;
  },
});

route.with()

Associates validation and content metadata with one handler while preserving the handler's inferred request and response types.

function with<O extends RouteOptions, R extends ResponseLike>(
  options: O,
  handler: RouteHandler<O, R>,
): WithResult<O, R>;

Parameters

Returns: A WithResult<O, R> consumed by route().

Throws: If body is present without contentType. At build time, Primate also rejects path schema keys that do not exactly match the route's dynamic segments and query schema keys that collide case-insensitively.

import p from "pema";
import route from "primate/route";

export default route({
  post: route.with({
    contentType: "application/json",
    body: p({ name: p.string }),
  }, async request => {
    const { name } = await request.body.json();
    return { name };
  }),
});

route.hook()

Types and returns a route hook function unchanged.

function hook(fn: HookFn): HookFn;

A hook receives the current request and next. It may return a response without calling next to stop the chain, or call next(request) and return its response (or another valid response). A hook must return a ResponseLike, may call next at most once, and must not call next and then return undefined.

// routes/admin/+hook.ts
import route from "primate/route";

export default route.hook(async (request, next) => {
  if (!request.session.get("user")) return new Response(null, { status: 401 });
  return next(request);
});

Hook files only. Export route.hook() from a +hook.js or +hook.ts file. A build fails if a hook function is exported from an ordinary route file.

Browser route methods

In a browser build, calling route(handlers) produces a ClientRoute<R> rather than exposing server handlers. A generated route import therefore acts as a typed endpoint client. Each HTTP method calls fetch, substitutes path parameters, appends query values, serializes the configured body, and resolves with the native Response.

import User from "@/routes/user/[id]";

const response = await User.get({
  path: { id: "42" },
  query: { expand: "profile" },
  signal: controller.signal,
});

Path and body arguments become required when their route options require them. A declared query schema also makes query required. Method functions expose their configured contentType; _result and Page are type-only inference properties and do not contain runtime values.

Method

Supported lowercase method names.

type Method =
  | "get"
  | "post"
  | "put"
  | "delete"
  | "head"
  | "connect"
  | "options"
  | "trace"
  | "patch";

RouteHandlers

type RouteHandlers = {
  [K in Method]?:
    | RouteHandler
    | WithResult<RouteOptions, ResponseLike>;
};

Under the browser export condition, this supporting map is string-indexed so the transformed module can retain the route source's keys. Server route construction still accepts only Method keys.

RouteHandler

type RouteHandler<
  O extends RouteOptions = RouteOptions,
  R extends ResponseLike = ResponseLike,
> = (request: NarrowedRequest<O>) => MaybePromise<R>;

Handlers may be synchronous or asynchronous. Thrown errors and rejected promises enter Primate's route error handling. Pema parse failures become a 400 JSON response; a request content-type mismatch becomes 415.

ResponseLike

Values accepted from route handlers and hooks.

type ResponseLike =
  | string
  | Dict
  | Dict[]
  | URL
  | Blob
  | ReadableStream
  | Streamable
  | Response
  | ResponseFunction<unknown, unknown>
  | null;

Strings become text responses; dictionaries and arrays become JSON; URLs become external redirects; binary/streamable values become binary responses; native Response objects pass through; and null becomes a 204 response. Response functions are the values produced by helpers from primate/response.

RouteOptions

type RouteOptions = {
  contentType?: MIMEValue;
  body?: Parsed<unknown> | AsyncType;
  path?: ObjectType | AsyncType;
  query?: ObjectType | AsyncType;
};

MIMEValue is a value from @rcompat/http's MIME constants. Request-body method narrowing is provided for JSON, text, URL-encoded, multipart, and octet-stream content types. Path and query schemas must be object-shaped.

NarrowedRequest

The request type passed to a handler after applying its route schemas.

type NarrowedRequest<O extends RouteOptions> =
  Omit<RequestFacade, "body" | "path" | "query"> & {
    body: NarrowedBody<O, RequestBody>;
    path: NarrowedPath<O>;
    query: NarrowedQuery<O>;
  };

See the RequestFacade API for the complete request member contract and the Requests guide for examples.

WithResult

The intermediate value returned by route.with().

type WithResult<O extends RouteOptions, R extends ResponseLike> = {
  handler: RouteHandler<O, R>;
  options: O;
};

The runtime value also carries an internal brand used by route(). Under the browser export condition its structural declaration instead includes an optional result?: R inference property; that property has no runtime value.

Routes

Maps each input method to its normalized server route method.

type Routes<R extends RouteHandlers> = {
  [K in keyof R]: R[K] extends WithResult<
    infer O extends RouteOptions,
    infer Result
  >
    ? RouteMethodWithResult<O, Result>
    : R[K] extends (...args: any[]) => infer Result
      ? RouteMethodWithResult<Record<never, never>, Result>
      : RouteMethod<Record<never, never>>;
};

RouteMethod

type RouteMethod<O extends RouteOptions> = {
  handler: (request: NarrowedRequest<O>) => unknown;
  options: O;
  Page: never;
};

type RouteMethodWithResult<O extends RouteOptions, R> =
  Omit<RouteMethod<O>, "Page"> & {
    Page: Page<R>;
  };

handler and options exist at runtime. Page is type-only and extracts the props accepted by a page response function; its runtime value is absent.

HookFn

type HookFn = (
  request: RequestFacade,
  next: (request: RequestFacade) => MaybePromise<ResponseLike>,
) => MaybePromise<ResponseLike>;

ClientRoute

The browser form of a route export.

type ClientRoute<R> = {
  [K in keyof R]: R[K] extends {
    options: infer O extends RouteOptions;
    result?: infer Result;
  }
    ? ClientMethod<O, Result>
    : R[K] extends (...args: any[]) => infer Result
      ? ClientMethod<Record<never, never>, Result>
      : (args?: RequestMeta<Record<never, never>>) => Promise<Response>;
};

ClientMethod

type ClientMethod<O extends RouteOptions, R = unknown> = {
  contentType?: string;
  _result?: ClientResult<R>;
  Page: Page<R>;
} & ClientMethodFunction<O>;

ClientMethodFunction<O> accepts an optional or required object composed from body, path, and RequestMeta<O>, according to O, and returns Promise<Response>.

RequestMeta

Browser request options shared by client route methods.

type RequestMeta<O extends RouteOptions> = {
  headers?: HeadersInit;
  signal?: AbortSignal;
} & (O extends { query: unknown }
  ? { query: Query<O> | URLSearchParams }
  : { query?: Query<O> | URLSearchParams });
Previous
Overview
Next
Request