paramour
Guides

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 = ();
const link: Href<"/about">

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 slug: string[]
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 nearest error.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:
app/product/[id]/page.tsx
// @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:

app/product/[id]/page.tsx
// @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 paramour

During 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.

On this page