Skip to content

ya2s/next-typed-url

Repository files navigation

next-typed-url

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 typed buildUrl implementation for each app
  • index.ts under each app directory: an ergonomic wrapper (webUrl, adminUrl, ...)
  • Root-level index.ts: re-exports per-app builders and provides an urls dictionary

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.

Install

npm i -D next-typed-url
# or
pnpm add -D next-typed-url
# or
yarn add -D next-typed-url

CLI Usage

# 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-url

Options

  • --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",
  }
}

Output API

For an app named web, the generator emits:

  • <out>/web/builder.ts (the raw buildUrl implementation and route types)
  • <out>/web/index.ts (exports webUrl with build, href, pathname helpers)
  • <out>/index.ts (re-exports each {app}Url; when a single app is generated urls equals that helper, otherwise it's a dictionary of helpers)
  • WebRoute, WebRouteInput, and WebRouteSearchParamsMap types 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 } });

Route Coverage

  • Included:
    • Pages Router: files under <root>/(src/)pages/** (single mode) or apps/<app>/(src/)pages/** (multi mode) with extensions .tsx, .ts, .jsx, .js (excluding _app, _document, _error, 500, and optionally 404).
    • App Router: files named page.{tsx,ts,jsx,js} or default.* under <root>/(src/)app/** (single) or apps/<app>/(src/)app/** (multi). Route groups (group) and parallel (@slot) directories are ignored when computing URL paths.
  • Excluded: pages/api/**, special pages _app, _document, _error, 500. Use --exclude-404 to also drop 404 in Pages Router mode. App Router-only helpers like loading, error, layout, route.ts are ignored.
  • Dynamic segments ([id], [...slug], [[...slug]]) produce typed params. Catch-all routes expect a non-empty array, optional catch-all routes accept undefined or an array. Index handling remains the same: /foo/index.tsx becomes /foo.
  • Search-parameter types: if you want typed search parameters, export type Query = { ... } (or interface Query) from the page module. The generator detects this export and imports it as the searchParams type for that route.

Query Types

  1. Inside the page module (e.g., apps/web/src/pages/product/[productID].tsx or apps/web/src/app/blog/[slug]/page.tsx) define export type Query = { ... } or export interface Query { ... }. The export name must always be Query.
  2. The generator imports that type with a statement such as import type { Query as xxx_Query } from './page' and wires it into WebRouteSearchParamsMap.
  3. 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.

Programmatic API

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",
});

Notes

  • 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/node is available.

Development

  • Format: pnpm format
  • Lint: pnpm lint
  • Test: pnpm test

About

Generate next-typesafe-url–style, typed URL builders for Next.js projects.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published