Defining Routes
Static, dynamic, and catch-all routes with defineAppRoute.
Recipes for declaring routes and parsing them on the server. If you haven't
read Route Objects as Currency, the one-line
version: define each route once in a route.def.ts beside its page, import
the object everywhere.
Static routes
No params, no config — but still worth defining, because a static route
object gives you a registry-checked, typo-proof href:
import { , } from "paramour";
export const = ("/about", {});
const link = ();When a route needs nothing, href's options are omittable entirely.
Dynamic segments
One codec per [segment], keyed by the segment name. The params config must
match the path's segments exactly — a missing or misnamed key is a compile
error:
import { , } from "paramour";
export const = ("/orders/[orderId]/items/[line]", {
: {
: .(),
: .(),
},
});Any codec works in a param position — p.index() here gives 1-based URLs
(/orders/A1/items/1) over 0-based array indices in memory.
Catch-all segments
A catch-all's codec describes one segment element; the array-ness comes
from the segment kind. [...slug] (required) decodes to a non-empty array;
[[...slug]] (optional) also matches the bare path and decodes it to []:
import { , , } from "paramour";
export const = ("/docs/[[...slug]]", {
: { : .() },
});
declare const : import("paramour").;
const { slug } = await .();
const = (, { : { : ["guides", "hooks"] } });
const = (, { : { : [] } });
// => "/docs/guides/hooks" and "/docs"(This exact route serves the page you're reading.)
Parsing on the server
App routes expose two parse styles, and the choice is about who made the mistake:
parse— throws a structured error to your nearesterror.tsx. Right when a malformed URL is exceptional and a generic error page is the correct answer.safeParse— returns{ status: "success" | "error" }. Right when you want to decide — most commonly, treating a bad URL as a 404:
// @filename: app/product/[id]/route.def.ts
import { , } from "paramour";
export const = ("/product/[id]", {
: { : .() },
: { : .().(1) },
});
// @filename: app/product/[id]/page.tsx
import type { RouteProps } from "paramour";
import { } from "next/navigation";
import { } from "./route.def";
export default async function (: RouteProps) {
const = await .();
if (. === "error") ();
const { , } = .;
return (
<>
Product #{.}, page {.}
</>
);
}Both styles come in halves, too: parseParams / parseSearch (and their
safe* twins) when a call site only needs one side. generateMetadata is
the classic case:
// @filename: app/product/[id]/route.def.ts
import { , } from "paramour";
export const = ("/product/[id]", {
: { : .() },
});
// @filename: app/product/[id]/page.tsx
import type { Metadata } from "next";
import type { RouteProps } from "paramour";
import { } from "./route.def";
export async function (: RouteProps): <Metadata> {
const { } = await .();
return { : `Product #${()}` };
}Parsing and metadata see identical decoding — there is one definition of
what /product/42 means.
Pages Router
definePagesRoute declares routes for pages/ with the same config shape;
parsing happens through the sync, context-based parseContext /
safeParseContext in getServerSideProps and friends. The
hooks guide walks through the full Pages
surface, client side included.
After adding a route
Regenerate the registry so the new path joins the compile-time union:
npm run paramourDuring next dev the withTypedRoutes wrapper does this automatically;
paramour generate --watch covers non-dev workflows. See
CLI workflows for the drift-checking story in CI.