DEV Community

Cover image for I built a typed named-route helper for Next.js, Nuxt, SvelteKit, and more
Elielton Freitas Benacci
Elielton Freitas Benacci

Posted on

I built a typed named-route helper for Next.js, Nuxt, SvelteKit, and more

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}`;
Enter fullscreen mode Exit fullscreen mode

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 Named Routes homepage showing the package introduction and route departure board

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Wrap your existing Next.js configuration:

// next.config.ts
import { withNamedRoutes } from "@triooo/named-routes/next";

export default withNamedRoutes({
  reactStrictMode: true,
});
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 website installation guide showing the steps from installation to a first link

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",
  },
});
Enter fullscreen mode Exit fullscreen mode

Then use:

route("users.show", { user: 42 });
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()],
});
Enter fullscreen mode Exit fullscreen mode

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.

The website network map connecting supported framework ecosystems

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
Enter fullscreen mode Exit fullscreen mode

Catch-all parameters use arrays to represent multiple segments:

// app/docs/[...slug]/page.tsx
route("docs.$slug", {
  slug: ["guides", "installation"],
});
// /docs/guides/installation
Enter fullscreen mode Exit fullscreen mode

For an optional catch-all page such as app/docs/[[...slug]]/page.tsx, the remainder can be omitted:

route("docs.$slug");
// /docs
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Array query values repeat the key. null and undefined query values are omitted.

The interactive workbench showing the relationship between page files, route names, parameters, and generated URLs

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",
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Assuming an /about page exists:

route("about");
// /about

route("about", {}, { locale: "en" });
// /about

route("about", {}, { locale: "pt" });
// /pt/sobre
Enter fullscreen mode Exit fullscreen mode

You can also bind a helper to a locale:

const pt = route.forLocale("pt");

pt("users.$user", { user: 42 });
// /pt/users/42
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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" } },
);
Enter fullscreen mode Exit fullscreen mode
route("users.$user", { user: 42 }, { absolute: true });
// https://example.com/admin/users/42
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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() returns string. With Next.js typedRoutes, the separate Link type check may require a Route assertion; 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 .mjs configuration on older supported versions.

The website compatibility table describing integration coverage for the supported frameworks

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
Enter fullscreen mode Exit fullscreen mode

Which routing convention or URL-generation edge case would you want to see covered next?

Top comments (0)