Home / Articles / A Single Source of Truth for Route Paths in Next.js Applications

This article is published in English.

A Single Source of Truth for Route Paths in Next.js Applications

Learn how a typed central routes module replaces scattered string paths in Next.js, with helper functions for dynamic segments and grouped route exports.

1222 words

Most Next.js projects start by typing paths straight into Link components and router.push calls. That is fast for the first few pages, but as the app grows, the same strings get duplicated across navbars, footers, redirects and emails, and every rename becomes a search-and-replace exercise. The problem gets worse when AI coding assistants generate part of the code, because they rarely follow a single convention consistently. Moving every path into one typed module fixes that.

What goes wrong with inline paths

Here is a typical navigation component where every destination is a literal string:

// ❌ Problematic approach

import Link from 'next/link'
import { useRouter } from 'next/router'

export default function Navbar() {
  const router = useRouter()

  const handleNavigation = () => {
    router.push('/dashboard/user/profile')
  }

  return (
    <nav>
      <Link href="/about">About</Link>
      <Link href="/contact">Contact</Link>
      <Link href="/blog">Blog</Link>
      <Link href="/dashboard/analytics">Analytics</Link>
      <button onClick={handleNavigation}>Profile</button>
    </nav>
  )
}

Nothing here is broken today, but four problems appear as the codebase and team grow.

Renames become a hunt

Changing /blog to /articles means finding every occurrence across components, server code, tests and config. Missing one leaves a dead link.

Typos slip through

A path such as /dashbord is just a string, so neither the compiler nor the linter objects. The mistake typically surfaces as a 404, sometimes only in production.

The editor cannot help

Because routes are unstructured strings, there is no autocomplete, no "go to definition", and no warning when a route no longer exists.

Teammates drift apart

Different people, and different AI prompts, end up writing the same destination in slightly different ways: with or without a trailing slash, with a query string built by hand, and so on.

Centralising routes in one module

The fix is a single configuration file that every part of the app imports.

Where the file lives

A constants or config folder next to app and components keeps it easy to find:

/src
  /app
  /components
  /constants
    └── routes.ts
  /lib

Defining the routes object

The module below groups routes by area. Static pages are plain strings. Dynamic pages are small functions that take the variable segment and return the full path, so callers never assemble URLs by hand. API endpoints sit under a nested API key for use in fetch calls. The trailing as const makes TypeScript infer the exact literal values and marks the object read-only, which is what enables precise autocomplete and prevents accidental reassignment.

// constants/routes.ts
export const ROUTES = {
  // Public routes
  HOME: '/',
  ABOUT: '/about',
  CONTACT: '/contact',
  PRICING: '/pricing',

  // Auth routes
  LOGIN: '/auth/login',
  REGISTER: '/auth/register',
  FORGOT_PASSWORD: '/auth/forgot-password',

  // Blog routes
  BLOG: '/blog',
  BLOG_DETAIL: (slug: string) => `/blog/${slug}`,
  BLOG_CATEGORY: (category: string) => `/blog/category/${category}`,

  // Dashboard routes
  DASHBOARD: '/dashboard',
  DASHBOARD_ANALYTICS: '/dashboard/analytics',
  DASHBOARD_SETTINGS: '/dashboard/settings',
  DASHBOARD_PROFILE: '/dashboard/profile',

  // Dynamic user routes
  USER_PROFILE: (userId: string) => `/user/${userId}`,
  USER_POSTS: (userId: string) => `/user/${userId}/posts`,

  // API routes (useful for fetch calls)
  API: {
    USERS: '/api/users',
    POSTS: '/api/posts',
    AUTH: '/api/auth',
  },
} as const

// Export route groups for better organization
export const AUTH_ROUTES = {
  LOGIN: ROUTES.LOGIN,
  REGISTER: ROUTES.REGISTER,
  FORGOT_PASSWORD: ROUTES.FORGOT_PASSWORD,
} as const

export const DASHBOARD_ROUTES = {
  HOME: ROUTES.DASHBOARD,
  ANALYTICS: ROUTES.DASHBOARD_ANALYTICS,
  SETTINGS: ROUTES.DASHBOARD_SETTINGS,
  PROFILE: ROUTES.DASHBOARD_PROFILE,
} as const

The two extra exports, AUTH_ROUTES and DASHBOARD_ROUTES, are views over the main object for code that only cares about one area, such as middleware that protects the dashboard. Because they reference ROUTES rather than repeating strings, there is still just one definition per path.

One refinement worth considering: if a slug or ID can contain spaces or special characters, wrap it in encodeURIComponent inside the helper so every caller gets a safe URL automatically.

Using the routes in components

The navbar now imports names instead of strings. Both Link and imperative navigation read from the same module, so the two can never disagree:

// components/Navbar.tsx
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ROUTES, DASHBOARD_ROUTES } from '@/constants/routes'

export default function Navbar() {
  const router = useRouter()

  const handleProfileNavigation = () => {
    router.push(DASHBOARD_ROUTES.PROFILE)
  }

  return (
    <nav className="flex space-x-4">
      <Link href={ROUTES.ABOUT} className="hover:text-blue-500">
        About
      </Link>
      <Link href={ROUTES.CONTACT} className="hover:text-blue-500">
        Contact
      </Link>
      <Link href={ROUTES.BLOG} className="hover:text-blue-500">
        Blog
      </Link>
      <Link href={DASHBOARD_ROUTES.ANALYTICS} className="hover:text-blue-500">
        Analytics
      </Link>
      <button
        onClick={handleProfileNavigation}
        className="rounded bg-blue-500 px-4 py-2 text-white"
      >
        Profile
      </button>
    </nav>
  )
}

Note that this example imports useRouter from next/router, which belongs to the Pages Router. In App Router projects, import it from next/navigation inside a client component instead; the routes module works the same way with either.

Building dynamic links

Dynamic segments are where the pattern pays off most. The list below links to each post and its category by calling the helper functions with data from the post:

// components/BlogList.tsx
import Link from 'next/link'
import { ROUTES } from '@/constants/routes'

interface Post {
  slug: string
  title: string
  category: string
}

interface BlogListProps {
  posts: Post[]
}

export default function BlogList({ posts }: BlogListProps) {
  return (
    <div className="space-y-4">
      {posts.map((post) => (
        <article key={post.slug} className="rounded border p-4">
          <h2 className="text-xl font-bold">
            <Link href={ROUTES.BLOG_DETAIL(post.slug)} className="hover:text-blue-500">
              {post.title}
            </Link>
          </h2>
          <Link
            href={ROUTES.BLOG_CATEGORY(post.category)}
            className="text-sm text-gray-500 hover:text-blue-500"
          >
            Category: {post.category}
          </Link>
        </article>
      ))}
    </div>
  )
}

If the URL structure for posts ever changes, for example adding a year segment, you update BLOG_DETAIL once and every caller follows. TypeScript will also flag any call that passes the wrong number of arguments.

What you gain

  • Each path is defined once and reused everywhere.
  • Misspelled route names fail at compile time instead of producing 404s.
  • A URL change is a one-line edit.
  • The structure stays organised as the number of pages grows.
  • Editors provide autocomplete and type checking for every destination.
  • The same constants serve navbars, footers, sitemaps and breadcrumbs.
  • One file gives a quick overview of every page in the app.

Limits and alternatives

A constants file does not verify that a matching page actually exists in the app or pages directory. If a folder is renamed without updating the module, links still break. Next.js also offers a typed routes option that generates types from the file system; check the current documentation for its status and whether it fits your setup. Many teams combine both: generated types for correctness, and a constants module for readable names and helpers.

When AI assistants contribute code, tell them in your project instructions to import from the routes module.

Wrapping up

Treat URLs as part of your application's API rather than scattered strings. A typed, centralised routes module turns renames into single edits, surfaces mistakes in the editor, and gives humans and tools alike one obvious place to look.