Организация структуры маршрутов

Грамотно организованная структура маршрутов — ключевой элемент при работе с Universal Router. От того, как спроектированы маршруты, зависит читаемость кода, масштабируемость приложения и удобство поддержки.

В основе лежат три базовых принципа:

  • Иерархичность — маршруты должны отражать структуру приложения
  • Декомпозиция — каждый маршрут выполняет строго определённую задачу
  • Повторное использование — общие части маршрутов выносятся в отдельные модули

Базовая модель маршрута

В Universal Router маршрут представляет собой объект, содержащий:

  • путь (path)
  • обработчик (handler)
  • дочерние маршруты (children)
  • дополнительные метаданные

Простейший пример:

const routes = [
  {
    path: '/',
    handler: homeHandler
  },
  {
    path: '/about',
    handler: aboutHandler
  }
];

Каждый маршрут — это изолированная единица логики, которая может быть расширена при необходимости.


Вложенные маршруты

Иерархия маршрутов реализуется через свойство children. Это позволяет описывать сложные структуры URL без дублирования.

const routes = [
  {
    path: '/users',
    handler: usersHandler,
    children: [
      {
        path: '/:id',
        handler: userProfileHandler
      },
      {
        path: '/:id/edit',
        handler: userEditHandler
      }
    ]
  }
];

Особенности вложенности

  • Дочерние маршруты автоматически наследуют родительский путь
  • Можно строить глубокие уровни вложенности
  • Каждый уровень может иметь собственную логику

Разделение маршрутов по модулям

В крупных приложениях маршруты нельзя хранить в одном файле. Используется модульная структура:

// routes/users.js
export const userRoutes = {
  path: '/users',
  children: [
    {
      path: '/:id',
      handler: userProfileHandler
    }
  ]
};
// routes/index.js
import { userRoutes } from './users.js';
import { productRoutes } from './products.js';

export const routes = [
  userRoutes,
  productRoutes
];

Преимущества

  • Улучшение читаемости
  • Изоляция логики
  • Упрощение тестирования

Динамические сегменты

Dynamic segments позволяют обрабатывать переменные части URL:

{
  path: '/products/:productId',
  handler: productHandler
}

В обработчике параметры доступны через контекст:

async function productHandler(context) {
  const { productId } = context.params;
  return getProduct(productId);
}

Рекомендации

  • Использовать понятные имена параметров
  • Избегать перегрузки маршрутов большим количеством параметров
  • Проверять входные значения

Группировка маршрутов

Для логической организации используется группировка:

const adminRoutes = {
  path: '/admin',
  children: [
    {
      path: '/users',
      handler: adminUsersHandler
    },
    {
      path: '/settings',
      handler: adminSettingsHandler
    }
  ]
};

Подходы к группировке

  • По функциональности (users, products, orders)
  • По ролям (admin, public)
  • По областям приложения (dashboard, auth)

Middleware и цепочки обработки

Universal Router позволяет внедрять промежуточные обработчики:

async function authMiddleware(context, next) {
  if (!context.user) {
    throw new Error('Unauthorized');
  }
  return next();
}

Применение:

{
  path: '/dashboard',
  action: authMiddleware,
  children: [
    {
      path: '',
      handler: dashboardHandler
    }
  ]
}

Особенности

  • Middleware выполняется последовательно
  • Может изменять контекст
  • Может прерывать выполнение

Переиспользование маршрутов

Повторяющиеся шаблоны маршрутов выносятся в функции:

function createCrudRoutes(basePath, handlers) {
  return {
    path: basePath,
    children: [
      { path: '', handler: handlers.list },
      { path: '/create', handler: handlers.create },
      { path: '/:id', handler: handlers.view },
      { path: '/:id/edit', handler: handlers.edit }
    ]
  };
}

Использование:

const productRoutes = createCrudRoutes('/products', {
  list: productListHandler,
  create: productCreateHandler,
  view: productViewHandler,
  edit: productEditHandler
});

Ленивые маршруты (Lazy Loading)

Для оптимизации загрузки используются динамические импорты:

{
  path: '/reports',
  async handler() {
    const module = await import('./handlers/reports.js');
    return module.default();
  }
}

Преимущества

  • Уменьшение начального размера бандла
  • Быстрая загрузка критичных частей приложения
  • Разделение кода

Обработка ошибок в маршрутах

Ошибки должны обрабатываться централизованно:

const router = new UniversalRouter(routes, {
  errorHandler: (error, context) => {
    console.error(error);
    return renderErrorPage(error);
  }
});

Практика

  • Не обрабатывать ошибки в каждом маршруте отдельно
  • Использовать единый механизм логирования
  • Возвращать понятные пользователю сообщения

Расширение маршрутов через метаданные

Маршруты могут содержать дополнительные поля:

{
  path: '/profile',
  handler: profileHandler,
  meta: {
    requiresAuth: true,
    title: 'User Profile'
  }
}

Использование метаданных:

async function middleware(context, next) {
  if (context.route.meta.requiresAuth && !context.user) {
    throw new Error('Unauthorized');
  }
  return next();
}

Нормализация путей

Важно соблюдать единый стиль написания путей:

  • Использовать слеш в начале (/path)
  • Избегать дублирующих слешей
  • Следить за консистентностью вложенных маршрутов

Пример ошибки:

path: 'users' // плохо

Правильный вариант:

path: '/users'

Глубокая композиция маршрутов

Сложные приложения используют композицию:

const routes = [
  {
    path: '/',
    children: [
      publicRoutes,
      authRoutes,
      adminRoutes
    ]
  }
];

Результат

  • Чёткое разделение зон ответственности
  • Возможность независимого развития модулей
  • Простота расширения

Стратегии масштабирования

При росте приложения применяются:

  1. Feature-based структура

    • каждый модуль содержит свои маршруты
  2. Domain-driven подход

    • маршруты соответствуют бизнес-доменам
  3. Layered architecture

    • разделение на UI, бизнес-логику и инфраструктуру

Практические ошибки

Избыточная вложенность

Слишком глубокая структура:

/users/:id/settings/security/password/change

Усложняет поддержку и понимание.


Дублирование маршрутов

{ path: '/users/list' }
{ path: '/users/all' }

Лучше использовать единый маршрут.


Смешивание логики

Маршрут не должен:

  • содержать бизнес-логику
  • выполнять сложные вычисления
  • напрямую работать с API

Подход к тестированию структуры маршрутов

  • проверка соответствия URL → handler
  • тестирование параметров
  • проверка middleware
  • изоляция модулей

Пример:

test('routes to user profile', async () => {
  const result = await router.resolve('/users/123');
  expect(result).toBeDefined();
});

Организация файловой структуры

Пример:

/routes
  /auth
    login.js
    register.js
  /users
    index.js
    profile.js
  index.js

Каждая директория:

  • содержит маршруты одной области
  • экспортирует единый объект
  • не зависит от других модулей напрямую

Связь маршрутов и состояния приложения

Контекст маршрута часто включает:

  • текущего пользователя
  • параметры URL
  • глобальное состояние
const router = new UniversalRouter(routes, {
  context: {
    user: currentUser,
    store
  }
});

Маршруты становятся частью общей архитектуры, а не изолированной системой.