Most applications already have a source of truth for their routes: the pages in their repository.
Yet application code often ends up maintaining a second, informal routing system through strings:
const href = `/users/${userId}`; That works until the same URL appears in navigation components, redirects, emails, and tests. A parameter gets renamed. A URL gains a locale prefix. Someone copies a path and forgets to encode a value.
I built @triooo/named-routes to make those call sites easier to maintain, while keeping the framework's existing routing conventions.
The idea is simple:
Create a page. Get an inferred route name. Build URLs with typed parameters.
Version 1.0.0 establishes the stable contract for route naming, URL generation, typed parameters, the CLI, and the Next.js and Vite integrations.
The website introduces the core workflow: pages become names, and names become URLs.
From a page file to a typed URL
Consider this Next.js App Router page:
app/users/[user]/page.tsx The generator discovers it and infers the name users.$user.
Your application can then write:
import { route } from "@/routes.generated"; route("users.$user", { user: 42 }); // /users/42 The generated helper knows which route names exist and which parameters each one requires.
route("users.$user", { user: 42 }); // OK // These calls produce TypeScript errors: route("users.$user"); route("users.$user", { id: 42 }); route("users.doesNotExist"); The runtime also validates calls. JavaScript consumers still get errors for unknown routes, missing parameters, and invalid values.
The helper returns a normal URL string, so it can be used wherever a URL string is expected.
Getting started with Next.js
Install the package:
npm install @triooo/named-routes Wrap your existing Next.js configuration:
// next.config.ts import { withNamedRoutes } from "@triooo/named-routes/next"; export default withNamedRoutes({ reactStrictMode: true, }); The integration generates src/routes.generated.ts before development or build starts. During development, it keeps the generated routes current as page files change.
You can use the helper in a component:
import Link from "next/link"; import { route } from "@/routes.generated"; export function UserLink({ userId }: { userId: number }) { return ( <Link href={route("users.$user", { user: userId })}> View user </Link> ); } This import assumes @/* maps to src/*. Adjust it to your application's aliases if necessary.
Next.js App Router and Pages Router are both supported, including src layouts and projects migrating between the two. Discovery can scan both directories; conflicting canonical URLs fail generation.
The Next.js integration needs one configuration wrapper before you can import the generated helper.
How automatic names work
Names follow the URL structure:
| Page file | Inferred name |
|---|---|
app/page.tsx | home |
app/user/profile/page.tsx | user.profile |
app/users/[user]/page.tsx | users.$user |
app/posts/[post]/edit/page.tsx | posts.$post.edit |
app/docs/[...slug]/page.tsx | docs.$slug |
Static segments are joined with dots. Parameters use a $ prefix. Framework markers such as page.tsx, route groups, and pathless layouts do not become public name segments.
This also makes the tradeoff explicit: moving a URL can change its inferred name.
Sometimes that is helpful. Once the generated module updates, TypeScript can point out call sites that still reference the old name.
Sometimes you want a semantic name that survives a URL change. You can configure that explicitly:
// named-routes.config.mjs import { defineConfig } from "@triooo/named-routes/config"; export default defineConfig({ names: { "/users/[user]": "users.show", }, }); Then use:
route("users.show", { user: 42 }); A custom name replaces the automatically inferred name for that path. It does not create an additional alias. If the URL changes later, update the mapping to preserve users.show.
One helper across multiple framework conventions
The package includes convention adapters for:
- Next.js App Router and Pages Router
- Nuxt
- Vue Router file-based routing
- SvelteKit
- Astro
- React Router / Remix flat routes
- TanStack Router
For example, these files share the same inferred route name:
Next.js: app/users/[user]/page.tsx Nuxt: pages/users/[user].vue SvelteKit: src/routes/users/[user]/+page.svelte Remix: app/routes/users.$user.tsx → users.$user For Vite-based applications, add the plugin:
// vite.config.ts import { defineConfig } from "vite"; import { namedRoutes } from "@triooo/named-routes/vite"; export default defineConfig({ plugins: [namedRoutes()], }); Keep your existing framework plugins alongside it. Nuxt and Astro expose Vite plugin configuration through their own config files.
The framework can be detected from package.json. You can also select it explicitly when detection is ambiguous, such as in a monorepo.
These adapters read file conventions. They do not evaluate page code or discover every route declaration a framework might support.
Different file conventions feed the same named-route runtime.
URL parameters need more than string interpolation
A URL helper should handle values consistently.
Scalar parameters are encoded as individual segments:
route("users.$user", { user: "Ana Silva" }); // /users/Ana%20Silva route("users.$user", { user: "a/b" }); // /users/a%2Fb Catch-all parameters use arrays to represent multiple segments:
// app/docs/[...slug]/page.tsx route("docs.$slug", { slug: ["guides", "installation"], }); // /docs/guides/installation For an optional catch-all page such as app/docs/[[...slug]]/page.tsx, the remainder can be omitted:
route("docs.$slug"); // /docs The runtime rejects invalid values, including empty required parameters, non-finite numbers, and standalone . or .. segments.
Query strings and fragments belong in the options argument:
route("users.$user", { user: 42 }, { query: { page: 2, tag: ["typescript", "nextjs"], draft: false, }, hash: "activity", }); // /users/42?page=2&tag=typescript&tag=nextjs&draft=false#activity Array query values repeat the key. null and undefined query values are omitted.
The website includes an interactive workbench for exploring the file-to-name-to-URL flow.
Internationalization is part of v1
Localized URLs often involve more than prepending a language code.
You may need a default locale without a prefix, translated pathnames, or different domains for different locales.
Here is a configuration with English and Portuguese:
// named-routes.config.mjs import { defineConfig } from "@triooo/named-routes/config"; export default defineConfig({ i18n: { locales: ["en", "pt"], defaultLocale: "en", localePrefix: "as-needed", pathnames: { "/about": { pt: "/sobre", }, }, }, }); Assuming an /about page exists:
route("about"); // /about route("about", {}, { locale: "en" }); // /about route("about", {}, { locale: "pt" }); // /pt/sobre You can also bind a helper to a locale:
const pt = route.forLocale("pt"); pt("users.$user", { user: 42 }); // /pt/users/42 When configured, locale values are typed, and the generated module exports RouteLocale.
The configuration shape is compatible with next-intl's routing configuration. The Next.js wrapper also inherits Pages Router i18n settings automatically.
There is an important integration detail: calling route() without a locale returns the internal path. That is useful when a framework-aware link already handles localization. Explicit { locale } is intended for URLs that need localization applied by the helper, such as plain anchors, emails, or sitemaps.
Applying both mechanisms can duplicate prefixes, so choose the behavior appropriate to the consumer.
Base paths and absolute URLs
Next.js links and router navigation already apply basePath, so relative helper URLs omit it by default.
For consumers such as plain anchors or fetch, request it explicitly:
route("users.$user", { user: 42 }, { basePath: true }); Absolute URLs require a configured public origin:
// next.config.ts import { withNamedRoutes } from "@triooo/named-routes/next"; export default withNamedRoutes( { basePath: "/admin" }, { defaults: { origin: "https://example.com" } }, ); route("users.$user", { user: 42 }, { absolute: true }); // https://example.com/admin/users/42 This is useful when building links outside framework navigation. The origin is configured explicitly rather than inferred from window or an incoming request.
Routes declared in code can use the runtime too
Automatic discovery is optional.
If your application declares routes manually, use createRoutes() with a literal manifest:
import { createRoutes } from "@triooo/named-routes"; const route = createRoutes({ "users.show": "/users/[user]", search: "/search/[[...terms]]", }); route("users.show", { user: 42 }); // /users/42 route("search", { terms: ["typed", "routes"] }); // /search/typed/routes The same runtime handles typed parameters and URL construction without a file scanner.
For discovered projects, extraRoutes can add explicit patterns for URLs the filesystem cannot describe, including rewrite-related paths.
Keeping generated routes current in CI
The CLI exposes the workflow independently of the framework plugins:
npx named-routes init npx named-routes generate npx named-routes list npx named-routes check npx named-routes watch check compares the expected output with the existing generated file without modifying it, and exits with code 1 if it is stale.
If you commit generated routes, run that check before a build step that regenerates them. Checking after regeneration would hide the fact that the committed file was stale.
Generation is deterministic and timestamp-free. It also refuses to overwrite a handwritten file at the configured output path.
What the package does—and where its responsibility ends
The runtime has zero runtime dependencies and no framework or Node.js imports. The generator stays in Node tooling and reads file paths without executing your pages.
The package builds URLs. Your framework still handles route matching, rendering, redirects, and requests.
A few boundaries matter when evaluating it:
- Custom paths declared inside page code need explicit mappings or a manual manifest.
- A valid generated URL does not prove that a resource exists or that a user can access it.
-
route()returnsstring. With Next.jstypedRoutes, the separateLinktype check may require aRouteassertion; named-route names and parameters remain checked by this package. - Tooling requires Node.js 20.19+, and exported types require TypeScript 5+. TypeScript configuration files require Node.js 22.18+; use
.mjsconfiguration on older supported versions.
The compatibility table distinguishes integration and verification coverage across frameworks.
Try it and tell me where it helps
The documentation is available in English, Portuguese, Spanish, and Japanese, and the repository includes runnable framework examples.
If your application repeats URL strings across navigation, redirects, and localized links, I would love for you to try it:
npm install @triooo/named-routes Which routing convention or URL-generation edge case would you want to see covered next?




