Generate next-typesafe-url–style, typed URL builders for Next.js projects.
By default it scans a single Next.js root (./src/pages and/or ./src/app). Pass --apps <dir> to treat that folder as a collection of micro frontends (apps/<app>/(src/)pages, apps/<app>/(src/)app). In either mode it collects static & dynamic routes and emits:
builder.ts: the strongly typedbuildUrlimplementation for each appindex.tsunder each app directory: an ergonomic wrapper (webUrl,adminUrl, ...)- Root-level
index.ts: re-exports per-app builders and provides anurlsdictionary
The route parameter is a string-literal union, and dynamic segments require a typed params object so IDE autocomplete and type safety work out of the box. Search parameters follow Next.js's searchParams naming for familiarity.
npm i -D next-typed-url
# or
pnpm add -D next-typed-url
# or
yarn add -D next-typed-url# run with npx (single Next.js project)
npx next-typed-url --root . --out ./typed-url
# monorepo / micro frontend workspace
npx next-typed-url --apps ./apps --out ./packages/typed-url
# if installed as a devDependency
next-typed-url --root . --out ./typed-urlOptions
--root <dir>: Next.js project root (default: current working directory)--apps <dir>: treat the directory as a collection of apps (enables multi mode)--app-name <name>: override the inferred name for single mode--out <dir>: output directory for generated files (default:packages/next-typed-url)--exclude-404: exclude 404 routes
Tip: add it to your repository script, e.g.
{
"scripts": {
"urlgen": "next-typed-url --root . --out typed-url",
}
}For an app named web, the generator emits:
<out>/web/builder.ts(the rawbuildUrlimplementation and route types)<out>/web/index.ts(exportswebUrlwithbuild,href,pathnamehelpers)<out>/index.ts(re-exports each{app}Url; when a single app is generatedurlsequals that helper, otherwise it's a dictionary of helpers)WebRoute,WebRouteInput, andWebRouteSearchParamsMaptypes that capture both static/dynamic segments and search-param typings
Type and function shape (dynamic routes include an accompanying params type):
import type { Query as WebProductQuery } from "../../apps/web/src/pages/product/[productID]";
import type { Query as WebDocsQuery } from "../../apps/web/src/pages/docs/[...slug]";
export type WebRoute =
| "/"
| "/account"
| "/product/[productID]"
| "/docs/[...slug]";
type RouteParamValue = string | number;
type RouteParamArray = Array<RouteParamValue>;
type WebRouteSearchParamsMap = {
"/product/[productID]": WebProductQuery;
"/docs/[...slug]": WebDocsQuery;
};
export type WebRouteInput =
| { route: "/" }
| { route: "/account" }
| {
route: "/product/[productID]";
params: { productID: RouteParamValue };
searchParams?: WebRouteSearchParamsMap["/product/[productID]"];
}
| {
route: "/docs/[...slug]";
params: { slug: RouteParamArray };
searchParams?: WebRouteSearchParamsMap["/docs/[...slug]"];
};
export const buildUrl = (args: WebRouteInput) => {
const pathname = buildPath(args.route, args.params);
const search = buildSearchString(args.searchParams);
return {
pathname,
href: search ? `${pathname}${search}` : pathname,
...(search ? { search } : {}),
...(args.searchParams ? { searchParams: args.searchParams } : {}),
};
};Usage example (single project generated with npx next-typed-url --root . --out ./typed-url):
import { urls } from "@/typed-url"; // single mode: urls === webUrl
const url = urls.build({ route: "/account/point" });
// -> { pathname: '/account/point', href: '/account/point' }
const productUrl = urls.build({
route: "/product/[productID]",
params: { productID: 23 },
searchParams: { ref: "campaign-42" },
});
// -> {
// pathname: '/product/23',
// href: '/product/23?ref=campaign-42',
// search: '?ref=campaign-42',
// searchParams: { ref: 'campaign-42' }
// }
const docsUrl = urls.build({
route: "/docs/[...slug]",
params: { slug: ["guides", "advanced"] },
searchParams: { section: "utilities" },
});
// -> {
// pathname: '/docs/guides/advanced',
// href: '/docs/guides/advanced?section=utilities',
// search: '?section=utilities',
// searchParams: { section: 'utilities' }
// }
// In multi-app mode, `urls` exposes each app namespace.
const adminHref = urls.admin.href({ route: "/users/[id]", params: { id: 99 } });- Included:
- Pages Router: files under
<root>/(src/)pages/**(single mode) orapps/<app>/(src/)pages/**(multi mode) with extensions.tsx,.ts,.jsx,.js(excluding_app,_document,_error,500, and optionally404). - App Router: files named
page.{tsx,ts,jsx,js}ordefault.*under<root>/(src/)app/**(single) orapps/<app>/(src/)app/**(multi). Route groups(group)and parallel (@slot) directories are ignored when computing URL paths.
- Pages Router: files under
- Excluded:
pages/api/**, special pages_app,_document,_error,500. Use--exclude-404to also drop404in Pages Router mode. App Router-only helpers likeloading,error,layout,route.tsare ignored. - Dynamic segments (
[id],[...slug],[[...slug]]) produce typedparams. Catch-all routes expect a non-empty array, optional catch-all routes acceptundefinedor an array. Index handling remains the same:/foo/index.tsxbecomes/foo. - Search-parameter types: if you want typed search parameters, export
type Query = { ... }(orinterface Query) from the page module. The generator detects this export and imports it as thesearchParamstype for that route.
- Inside the page module (e.g.,
apps/web/src/pages/product/[productID].tsxorapps/web/src/app/blog/[slug]/page.tsx) defineexport type Query = { ... }orexport interface Query { ... }. The export name must always beQuery. - The generator imports that type with a statement such as
import type { Query as xxx_Query } from './page'and wires it intoWebRouteSearchParamsMap. - The generator does not provide runtime validation; it only surfaces type-safe autocomplete/checking. If you need runtime guards, implement them in the page module.
import { generate } from "next-typed-url";
// single project (default)
await generate({
appDir: ".",
outDir: "typed-url",
});
// multi-app workspace
await generate({
appsDir: "apps",
outDir: "packages/typed-url",
});- Targets the Next.js Pages Router and App Router (route groups/parallel routes are supported via folder semantics).
- Generated files are deterministic and safe to commit.
- Requires Node.js. If you build from source TypeScript, ensure
@types/nodeis available.
- Format:
pnpm format - Lint:
pnpm lint - Test:
pnpm test