paramour
Concepts

The Generated Route Registry

paramour generate turns your filesystem routes into a compile-time union.

paramour generate scans your app directory and emits a types-only module augmentation, so defining a route whose path doesn't exist on disk becomes a compile error — with zero runtime cost.

The problem it solves

A route object declares its path as a string: defineAppRoute("/product/[id]", …). The compiler can check everything about that string — params matching segments, codecs matching types — but it cannot know whether app/product/[id]/page.tsx actually exists. Filesystem truth lives outside the type system.

The registry imports it. paramour generate scans app/ (and pages/, if present) and writes paramour-env.d.ts:

paramour-env.d.ts
// Generated by @paramour-js/next. Do not edit — regenerate with `paramour generate`.
import "paramour";

declare module "paramour" {
  interface ParamourRegister {
    : "/" | "/product/[id]";
  }
}

That's the whole artifact: a types-only module augmentation teaching the compiler which paths exist, per router. No generated runtime code, no build step in your bundle — it compiles to nothing.

What it buys

Once the augmentation is in your program, the route constructors' path argument narrows from "any plausible string" to the registered union. Typos, renamed directories, and router mix-ups all become squiggles:

// @filename: paramour-env.d.ts
import "paramour";

declare module "paramour" {
  interface ParamourRegister {
    : "/" | "/product/[id]";
    : "/legacy/[id]";
  }
}

// @filename: routes.ts
import { , ,  } from "paramour";

("/totally/made/up", {});
Argument of type '"/totally/made/up"' is not assignable to parameter of type '"/" | "/product/[id]"'.
("/product/[productId]", {
Argument of type '"/product/[productId]"' is not assignable to parameter of type '"/" | "/product/[id]"'.
: { : .() }, }); ("/product/[id]", { : { : .() } });
Argument of type '"/product/[id]"' is not assignable to parameter of type '"/legacy/[id]"'.

The middle one is the case that pays rent: /product/[id] exists, but the definition says [productId] — exactly the near-miss that survives code review and fails at 2 a.m. The last shows the router gate: /product/[id] is an App route, so the Pages constructor rejects it.

Before the first generate, no augmentation exists and the constructors accept any well-formed path — the library degrades gracefully rather than blocking you. The registry is what upgrades "plausible" to "real."

The lifecycle

The artifact is generated and committed — treat it like a lockfile:

  1. Edit routes — add, move, or delete a page directory.
  2. paramour generate — rewrites paramour-env.d.ts (the "paramour" package script that init added runs exactly this). During next dev, the withTypedRoutes wrapper keeps it current automatically as routes appear and disappear.
  3. paramour check in CI — re-scans and compares. On drift it exits non-zero and never writes: a route deleted on disk but still registered (or vice versa) fails the build instead of lying to the compiler.

Committing the artifact means the registry works for everyone who checks out the repo — code review shows route changes as visible diffs to a small, readable file, and CI proves that diff honest. (Don't hand-edit or format it; it's regenerated verbatim.)

This site practices what this page preaches: every docs page you're reading is served by paramour routes, and the docs build runs paramour check before next build — registry drift here fails our CI too.

Where next

  • CLI workflowsgenerate, check, and the rest of the toolchain in depth, including watch mode and CI recipes.
  • Registry types referenceParamourRegister and the Registered*Paths lookups the artifact populates.
  • Getting Started — the registry in the context of a full setup, if you landed here first.

On this page