Головна / Статті / Єдине джерело істини для маршрутів у застосунках Next.js

Єдине джерело істини для маршрутів у застосунках Next.js

Дізнайтеся, як модуль централізованих маршрутів з введенням тексту замінює розкидані рядкові шляхи в Next.js, за допомогою функцій-допоміжників для динамічних сегментів та групового експорту маршрутів.

1222 слів

Більшість проєктів Next.js починаються з введення шляхів безпосередньо у компоненти Link та функції router.push. Це швидко для перших кількох сторінок, але коли додаток росте, однакові рядки починають дублюватися у навігаційних панелях, футерах, перенаправленнях та електронних листах, і кожна заміна назви стає складним завданням пошуку та заміни. Проблема погіршується, коли асистенти з кодування на основі ШІ генерують частину коду, оскільки вони рідко послідовно дотримуються єдиних правил. Перенесення всіх шляхів у один модуль вирішує цю проблему.

Що не так із вбудованими шляхами

Ось типовий компонент навігації, де кожна точка призначення є буквальним рядком:

// ❌ 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>
  )
}

Зараз тут нічого не не так, але з ростом кодової бази та команди з’являються чотири проблеми.

Заміна назв стає складним завданням

Заміна /blog на /articles означає необхідність пошуку кожного випадку використання цього шляху у компонентах, коді сервера, тестах та конфігурації. Якщо щось пропустити, залишиться недійсне посилання.

Друкарські помилки залишаються непоміченими

Шлях на кшталт /dashbord є просто рядком тексту, тому ні компілятор, ні інструменти перевірки не можуть його розпізнати. Помилка зазвичай проявляється як код 404, іноді лише у продакшен-середовищі.

Редактор не може допомогти

Оскільки маршрути є неструктурованими рядками, немає функції автодоповнення, можливості „перейти до визначення“ та попереджень про те, що маршрут більше не існує.

Члени команди розходяться у підходах

Різні люди та різні запити до ШІ призводять до того, що одну й ту саму мету записують трохи по-різному: з крапкою чи без неї, із вручну створеним параметром запиту тощо.

Централізація маршрутів у одному модулі

Рішення полягає у єдиному файлі конфігурації, який імпортується кожною частиною додатку.

Де знаходиться файл

Папка constants або config, розташована поруч із app та components, полегшує його пошук:

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

Визначення об’єкта маршрутів

Наведений нижче модуль групує маршрути за категоріями. Статичні сторінки — це звичайні рядки. Динамічні сторінки — це невеликі функції, які приймають змінну частину та повертають повний шлях, тож користувачі ніколи не створюють URL вручну. Кінцеві точки API розташовані під вкладеною ключем API для використання у викликах fetch. Додавання as const дозволяє TypeScript визначити точні літеральні значення та позначає об’єкт як лише для читання, що забезпечує точне автодоповнення та запобігає випадковому переатрибуванню.

// 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

Два додаткові експорти, AUTH_ROUTES та DASHBOARD_ROUTES, є представленнями основного об’єкта для коду, який цікавиться лише однією областю, наприклад мідлвейру, що захищає панель керування. Оскільки вони посилаються на ROUTES, а не повторюють рядки, залишається лише одне визначення на кожен шлях.

Одне вдосконалення, яке варто розглянути: якщо слаг або ID можуть містити пробіли чи спеціальні символи, потрібно обгорнути його у encodeURIComponent усередині допоміжної функції, щоб кожен користувач автоматично отримував безпечну URL.

Використання маршрутів у компонентах

Тепер navbar імпортує назви замість рядків. І Link, і імперативне навігування читають дані з того самого модуля, тому вони ніколи не можуть суперечити один одному:

// 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>
  )
}

Зауважте, що в цьому прикладі імпортується useRouter з next/router, який належить до Pages Router. У проектах App Router його слід імпортувати з next/navigation усередині клієнтського компонента; модуль маршрутів працює однаково в обох випадках.

Створення динамічних посилань

Динамічні сегменти — це те, де ця модель дає найбільшу користь. У списку нижче є посилання на кожну публікацію та її категорію шляхом виклику допоміжних функцій із даними з публікації:

// 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>
  )
}

Якщо структура URL для публікацій коли-небудь зміниться, наприклад, шляхом додавання сегмента з роком, вам потрібно буде оновити BLOG_DETAIL один раз, і всі виклики будуть слідувати за цим. TypeScript також позначить будь-який виклик, який передає неправильну кількість аргументів.

Що ви отримуєте

  • Кожен шлях визначається один раз та використовується скрізь.
  • Неправильно написані назви маршрутів призводять до помилок під час компіляції, замість того щоб створювати статуси 404.
  • Зміна URL — це редагування в одному рядку.
  • Структура залишається організованою зі зростанням кількості сторінок.
  • Редактори забезпечують автодоповнення та перевірку введених даних для кожної точки призначення.
  • Одні й ті самі константи використовуються для навігаційних барів, футерів, сайт-карт та слідчих ниток.
  • Один файл дає швидкий огляд усіх сторінок додатку.
  • Обмеження та альтернативи

    Файл констант не перевіряє, чи справді існує відповідна сторінка у каталозі app або pages. Якщо папку перейменувати без оновлення модуля, посилання все одно перестануть працювати. Next.js також пропонує опцію типованих маршрутів, яка генерує типи з файлової системи; перевірте поточну документацію щодо її статусу та того, чи підходить вона вашій конфігурації. Багато команд поєднують обидва підходи: генеровані типи для забезпечення коректності та модуль констант для зрозумілих імен та допоміжних функцій.

    Коли штучні інтелектуальні асистенти допомагають писати код, уточніть у інструкціях до проекту, що потрібно імпортувати з модуля маршрутів.

    Підсумок

    Розглядайте URL як частину API вашого додатку, а не просто розкидані рядки. Типований, централізований модуль маршрутів дозволяє здійснювати зміни через одне оновлення, виявляти помилки у редакторі та забезпечує як людям, так і інструментам одне очевидне місце для пошуку інформації.