paramour
Concepts

Route Objects as Currency

Imported objects, not string registries — tree-shaking stays perfect.

Every paramour route is a plain exported object you import wherever you build links or parse props. There is no central runtime registry — routes are currency, passed by import.

One definition, many consumers

A route object is created once, next to the page it describes:

app/product/[id]/route.def.ts
import { ,  } from "paramour";

export const  = ("/product/[id]", {
  : { : .() },
  : { : .().() },
});

Everything else in the system takes the object:

Because the route is the unit of exchange, the params/search contract is stated exactly once. Rename a param, change a codec, add a search key — every consumer's types update in the same keystroke, and anything now inconsistent fails to compile:

const link = (, { : { : 42 }, : { : "socks" } });
const link: Href<"/product/[id]">

Why not a string-keyed registry?

The other common design is a central map: register every route in one module, look routes up by name or path string. Paramour rejects that shape on purpose, for two reasons.

Tree-shaking. A central registry is a single object that references every route in the app — import one route and you've imported all of them, plus every codec they mention. With route objects as currency, a page that links to two routes bundles exactly those two. There is no paramour runtime state anywhere: the library is only the functions you call and the objects you import. (The generated registry is types-only and compiles to nothing.)

Refactorability. An imported object participates in every tool your editor already has — go-to-definition, find-all-references, rename-symbol. A string key participates in none of them.

Convention: route.def.ts beside the page

Route objects live in a route.def.ts file colocated with their page.tsx. The name is a convention, not a requirement — but colocation is worth keeping: the definition sits next to the file whose URL it describes, and moving the page directory moves its contract with it.

One real constraint: definition files should be import-safe — free of side effects and importable outside Next.js — because paramour list and the devtools panel evaluate them to inspect route shapes. A route.def.ts that only calls defineAppRoute/definePagesRoute is always safe; avoid dragging server-only modules into it.

Two constructors, two surfaces

Next.js has two routers with genuinely different data flows, and paramour does not paper over that. defineAppRoute produces a route whose parse surface is async and props-based (parse, parseParams, parseSearch, and their safe* twins). definePagesRoute produces one whose surface is sync and context-based (parseContext, safeParseContext), for getServerSideProps and friends.

The compiler enforces the split — an App-router method does not exist on a Pages route, and vice versa:

import { , ,  } from "paramour";

const  = ("/product/[id]", {
  : { : .() },
});
const  = ("/legacy/[id]", {
  : { : .() },
});

.parse;
Property 'parse' does not exist on type 'PagesRoute<"/legacy/[id]", { readonly id: Codec<number, "required", false, "single", boolean>; }, Record<never, never>>'.
.parseContext;
Property 'parseContext' does not exist on type 'AppRoute<"/product/[id]", { readonly id: Codec<number, "required", false, "single", boolean>; }, Record<never, never>>'.

The same gate runs through the hooks: hooks from @paramour-js/next/app accept only App routes, hooks from @paramour-js/next/pages only Pages routes. Mixing them up is a compile error, not a hydration mystery.

Where next

On this page