paramour

A routing companion your compiler approves of.

Validated route and search params, typed path building, and an explicit URL wire format for the Next.js App Router — with the Standard Schema validator you already use.

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

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

// typed, validated, explicit: "/product/42?q=paramour"
const link = (, { : { : 42 }, : { : "paramour" } });
const link: Href<"/product/[id]">
// a string into p.integer() fails to compile: href(, { : { : "42" } });
No overload matches this call. Overload 1 of 2, '(path: string, options?: StaticHrefOptions | undefined): Href<string>', gave the following error. Argument of type 'AppRoute<"/product/[id]", { readonly id: Codec<number, "required", false, "single", boolean>; }, { readonly q: Codec<string, "optional", false, "single", boolean>; }>' is not assignable to parameter of type 'string'. Overload 2 of 2, '(route: AppRoute<"/product/[id]", { readonly id: Codec<number, "required", false, "single", boolean>; }, { readonly q: Codec<string, "optional", false, "single", boolean>; }>, options: InferHrefInput<...>): Href<...>', gave the following error. Type 'string' is not assignable to type 'number'.
/product/42?q=paramour
params.id · p.integer() → "42"search.q · p.string() → "paramour"

The problem

The URL is an API. Most apps type it by hand.

Template literals compile no matter what they say, and everything read back from a URL is a string until proven otherwise.

By hand
declare const : { : number };
declare const : <string, string | undefined>;

// nothing is watching a template literal — the typo ships
const  = `/produtc/${.}?page=2`;

// every read is a guess
const  = (.); // NaN when absent
With paramour
import { , ,  } from "paramour";

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

// the path is real, the types are checked, absence has a rule
const  = (, {
  : { : 42 },
  : { : 2 },
});

Both of these compile. Only one of them is checked.

Features

Why paramour

Small pieces that agree with each other — and with your compiler.

Comparison

How it compares

Against next-typesafe-url and Next.js's built-in typedRoutes.

Featureparamournext-typesafe-urltypedRoutes
Typed path building
Route params validated at runtime
Search params validated at runtime
Validatorany Standard Schemazod
Library-owned serialization with a published spec
Hooks for reading params
CI drift check (check, doctor)
Bundled agent skill for coding agents
Devtools panel
nuqs adapter
Testing provider (no next/* mocks)
ESLint rule for raw hrefs

As of next-typesafe-url 5.x and the typedRoutes option in Next.js 16. Spotted something out of date? Open an issue.

Coming from next-typesafe-url?

The architecture barely moves — what changes is the vocabulary. Your route definitions stay colocated, your hooks keep their names, and $path({ route, ... }) becomes href(route, ...). The migration guide was written by migrating a real app, route by route.

Start with one route

pnpm add paramour @paramour-js/nextGet started