Олег Марков
Маршруты routes в веб разработке
Введение
Маршруты (routes) — это основа любого веб‑приложения. Каждый раз, когда вы открываете страницу по адресу /profile или отправляете запрос на /api/users, где‑то в серверном коде срабатывает маршрут, который решает, что делать с этим запросом.
Смотрите, я покажу вам, как удобный и продуманный роутинг:
- упрощает организацию кода;
- делает API понятным;
- помогает избежать дублирования логики;
- позволяет гибко обрабатывать разные типы запросов (GET, POST, PUT, DELETE и т.д.).
В этой статье мы разберем:
- общую идею маршрутизации;
- типы маршрутов (статические, динамические, с параметрами и шаблонами);
- методы HTTP и связь с маршрутами;
- группировку маршрутов и версии API;
- middleware и их роль в маршрутах;
- обработку ошибок и “404 Not Found”;
- советы по проектированию маршрутов.
Для примеров я буду использовать синтаксис, похожий на популярные веб‑фреймворки (Express.js / Node.js и немного обобщенного псевдокода). Вы легко сможете перенести эти идеи в любой язык и фреймворк: Go, Python, PHP, Java, .NET и т.д.
Что такое маршруты в веб‑приложении
Основная идея маршрутизации
Маршрут — это правило, которое связывает:
- шаблон URL (например
/users,/users/:id); - HTTP‑метод (GET, POST, PUT, DELETE и др.);
- обработчик (функцию, которая отвечает на запрос).
Если описать это упрощенно, то роутинг можно представить так:
// Псевдокод
router.on('GET', '/users', getUsersHandler)
router.on('POST', '/users', createUserHandler)
router.on('GET', '/users/:id', getUserByIdHandler)
- Строка
/users— это путь (route path). 'GET','POST'— это HTTP‑методы.getUsersHandlerи другие — функции‑обработчики.
Как только пользователь обращается к /users с методом GET, роутер ищет совпадающий маршрут и вызывает соответствующий обработчик.
Зачем вообще нужен роутинг
Давайте разберемся, что вы получаете, используя продуманные маршруты.
Разделение логики по URL.
Логика работы с пользователями — по одному адресу, с заказами — по другому и т.д.Читаемое API.
Путь/api/users/123/ordersсразу говорит, что происходит.Управляемость и масштабируемость.
Когда приложение растет, вы можете группировать маршруты по модулям, версиям, доменам ответственности.Безопасность и контроль доступа.
Через роутер удобно навесить аутентификацию и авторизацию только на определенные маршруты (например,/admin/*).
Типы маршрутов
Статические маршруты
Статический маршрут — это путь, который сопоставляется точно в точку: адрес должен совпадать полностью.
Пример:
// Здесь мы объявляем статический маршрут для главной страницы
app.get('/', (req, res) => {
// Отправляем HTML или шаблон
res.send('Главная страница')
})
// Статический маршрут для страницы "О нас"
app.get('/about', (req, res) => {
res.send('О нас')
})
Особенности:
- Нет переменных частей.
- Удобны для простых страниц:
/,/about,/contacts.
Если маршрут /about определен как статический, запрос на /about/ (с лишним слэшем в конце) в некоторых фреймворках уже может не совпасть, если вы явно не включите “lenient routing”. На это стоит обратить внимание, когда вы проектируете схемы URL.
Динамические маршруты и параметры пути
Часто вам нужно принимать значения прямо из URL: например, ID пользователя или кода товара. Для этого используются динамические сегменты (route parameters).
Давайте посмотрим на примере:
// Здесь :id - динамический параметр маршрута
app.get('/users/:id', (req, res) => {
const userId = req.params.id // Читаем значение параметра из URL
res.send(`Информация о пользователе с id = ${userId}`)
})
Если клиент запросит /users/42, фреймворк подставит 42 в req.params.id.
Особенности параметров пути:
- Параметр обычно обозначается через двоеточие и имя:
:id,:slug,:orderId. - В большинстве фреймворков параметры считаются обязательными: если вы не передали
id, маршрут не совпадет. - Можно использовать несколько параметров в одном маршруте.
Пример с несколькими параметрами:
// Маршрут с двумя параметрами в пути
app.get('/users/:userId/orders/:orderId', (req, res) => {
const { userId, orderId } = req.params // Деструктурируем оба параметра
// Здесь вы можете, например, загрузить заказ пользователя
res.send(`Заказ ${orderId} пользователя ${userId}`)
})
Необязательные и “хвостовые” параметры
Некоторые фреймворки поддерживают необязательные сегменты в пути.
Пример (синтаксис может отличаться в вашем фреймворке):
// Пример для фреймворка с поддержкой необязательных сегментов
app.get('/products/:category?', (req, res) => {
const category = req.params.category // Может быть undefined
if (category) {
res.send(`Товары категории ${category}`)
} else {
res.send('Все товары')
}
})
Также встречаются так называемые “catch‑all” или “wildcard” сегменты — они захватывают остаток пути.
Пример:
// *path - захватывает всю оставшуюся часть URL
app.get('/files/*path', (req, res) => {
const fullPath = req.params.path // Например, images/avatars/user1.png
// Здесь можно отдать файл или выполнить другую логику
res.send(`Вы запросили файл по пути ${fullPath}`)
})
Обратите внимание: использование catch‑all маршрутов удобно, но может вести к конфликтам и неожиданному поведению, если вы не продумали порядок объявлений маршрутов.
Маршруты с шаблонами и ограничениями
Иногда вам нужно не просто принять произвольную строку, а ограничить формат параметра (например, только числа). Многие роутеры позволяют задавать шаблоны или регулярные выражения прямо в определении маршрута.
Пример с регулярным выражением:
// Псевдосинтаксис маршрута, где id должен быть числом
app.get('/users/:id(\\d+)', (req, res) => {
const id = Number(req.params.id) // Здесь мы уверены, что это число
res.send(`Пользователь с числовым id = ${id}`)
})
Здесь я показываю пример, чтобы вы увидели, как шаблон \d+ ограничивает параметр только цифрами. Если вы запросите /users/abc, маршрут не сработает, и будет выбрано другое, более подходящее правило (или вернется 404).
HTTP‑методы и маршруты
Связь метода и маршрута
Маршрут всегда определяется в связке:
- путь;
- HTTP‑метод.
То есть /users с методом GET и /users с методом POST — это два разных маршрута с разной логикой.
Давайте посмотрим на классический пример CRUD (Create, Read, Update, Delete) для сущности users:
// Получить список пользователей (Read - список)
app.get('/users', (req, res) => {
// Здесь обычно запрашивают данные из базы
res.send('Список пользователей')
})
// Создать нового пользователя (Create)
app.post('/users', (req, res) => {
// Здесь вы читаете данные из тела запроса и создаете запись
res.status(201).send('Пользователь создан')
})
// Получить одного пользователя по id (Read - один объект)
app.get('/users/:id', (req, res) => {
res.send(`Пользователь с id = ${req.params.id}`)
})
// Обновить данные пользователя (Update)
app.put('/users/:id', (req, res) => {
res.send(`Пользователь с id = ${req.params.id} обновлен`)
})
// Частично обновить данные пользователя (частичный Update)
app.patch('/users/:id', (req, res) => {
res.send(`Пользователь с id = ${req.params.id} частично обновлен`)
})
// Удалить пользователя (Delete)
app.delete('/users/:id', (req, res) => {
res.send(`Пользователь с id = ${req.params.id} удален`)
})
Как видите, адресная часть повторяется, но метод меняется, а вместе с ним меняется и смысл операции.
RESTful подход к проектированию маршрутов
Часто вы будете слышать термины “REST API” и “RESTful маршруты”. Это просто соглашения, как называть и организовывать URL:
- существительное во множественном числе — для коллекций (
/users); - путь с
/:id— для конкретного ресурса (/users/42); - метод выбирает действие (GET, POST, PUT, DELETE и т.д.);
- не писать глаголы в URL (избегать
/getUsers,/createUserи т.п.).
Базовый набор для ресурса articles:
GET /articles— список статей;POST /articles— создать статью;GET /articles/:id— получить одну;PUT/PATCH /articles/:id— обновить;DELETE /articles/:id— удалить.
Такой подход делает маршруты предсказуемыми и единообразными, что важно в крупных проектах и для командной работы.
Параметры запросов и тело запроса
Маршрут — это не только путь, но и то, откуда вы берете данные для обработки.
Параметры строки запроса (query string)
Часть URL после знака ? называется строкой запроса (query). Внутри нее передаются параметры запроса, которые чаще всего используются для фильтрации, сортировки, пагинации.
Пример URL:
/users?limit=10&offset=20&sort=name
Здесь:
- путь маршрута —
/users; - параметры запроса —
limit,offset,sort.
Пример обработки:
// Маршрут для получения списка пользователей с фильтрацией
app.get('/users', (req, res) => {
const limit = Number(req.query.limit) || 10 // Максимум элементов на странице
const offset = Number(req.query.offset) || 0 // Смещение для пагинации
const sort = req.query.sort || 'name' // Поле сортировки
// Здесь обычно выполняется запрос к базе с учетом параметров
res.send(
`Список пользователей limit=${limit}, offset=${offset}, sort=${sort}`
)
})
Обратите внимание: параметры query не входят в путь, поэтому один маршрут /users может обрабатывать множество вариантов адреса (/users?limit=10, /users?active=true и т.д.).
Параметры тела запроса (body)
Для маршрутов с методами POST, PUT, PATCH чаще всего данные передаются в теле запроса (body), а не в URL.
Пример:
// Маршрут для создания пользователя
app.post('/users', (req, res) => {
const { name, email } = req.body // Читаем данные из тела запроса
// Здесь вы можете выполнить валидацию и сохранить пользователя в базе
res.status(201).send(`Пользователь ${name} с email ${email} создан`)
})
Здесь я размещаю пример, чтобы вам было проще понять:
- путь
/usersопределяет, с какой сущностью мы работаем; - метод POST говорит, что мы создаем новую запись;
- тело запроса содержит данные, которые нужно сохранить.
Порядок маршрутов и конфликтующие правила
Почему важен порядок объявления
Во многих фреймворках маршруты проверяются в том порядке, в котором вы их объявили. Поэтому более “общие” маршруты должны идти после более “специфичных”, иначе они перехватят запрос раньше.
Давайте посмотрим, что происходит в таком примере:
// Менее удачный порядок
// 1. Объявляем общий маршрут
app.get('/users/:id', (req, res) => {
res.send(`Пользователь ${req.params.id}`)
})
// 2. Объявляем более точный маршрут
app.get('/users/me', (req, res) => {
res.send('Текущий пользователь')
})
Если в роутере нет дополнительной логики приоритета, запрос на /users/me попадет в первый маршрут, и id будет равно строке me. Вторая функция никогда не вызовется.
Правильнее записать так:
// Более удачный порядок
// 1. Сначала конкретный маршрут
app.get('/users/me', (req, res) => {
res.send('Текущий пользователь')
})
// 2. Потом общий маршрут с параметром
app.get('/users/:id', (req, res) => {
res.send(`Пользователь ${req.params.id}`)
})
Теперь запрос на /users/me будет обработан правильно, а все прочие, вроде /users/42, пойдут во второй обработчик.
Конфликты с “catch‑all” маршрутами
Маршруты вида /files/*path или просто /* часто используются для:
- SPA‑приложений (одностраничников), где все пути отдаются одним HTML‑файлом;
- логирования или отладки;
- универсального перехвата.
Важно не ставить такие маршруты слишком рано, иначе они поглотят все остальные.
Пример проблемы:
// Плохо - catch-all объявлен раньше конкретных маршрутов
app.get('*', (req, res) => {
res.send('SPA приложение')
})
app.get('/api/users', (req, res) => {
res.send('Список пользователей') // Этот обработчик никогда не вызовется
})
Лучше объявлять catch‑all в самом конце, когда все конкретные правила уже зарегистрированы.
Группировка маршрутов и модульная структура
Зачем группировать маршруты
Когда маршрутов становится десятки и сотни, вам становится сложно работать с ними, если они лежат в одном файле. Проще воспринимать, когда:
- маршруты разбиты по доменам (users, orders, products и т.д.);
- логика и роуты модуля хранятся рядом;
- у групп маршрутов есть общие префиксы (
/api,/adminи т.п.).
Пример группировки по префиксу
Давайте посмотрим простой пример с префиксом /api:
// Создаем "подроутер" для API
const apiRouter = express.Router()
// Здесь описываем маршруты API
apiRouter.get('/users', (req, res) => {
res.send('API - список пользователей')
})
apiRouter.post('/users', (req, res) => {
res.send('API - создать пользователя')
})
// Подключаем группу маршрутов с префиксом /api
app.use('/api', apiRouter)
// Теперь реальные пути: /api/users (GET, POST и т.д.)
Обратите внимание:
- Внутри
apiRouterмы пишем пути так, как будто/apiне существует. - При подключении
app.use('/api', apiRouter)все маршруты этого роутера получают префикс/api.
Разделение маршрутов по файлам
Структура проекта может выглядеть так:
routes/users.js— все маршруты, связанные с пользователями;routes/orders.js— все маршруты, связанные с заказами;routes/index.jsилиapp.js— сборка всех маршрутов.
Пример файла routes/users.js:
// routes/users.js
const express = require('express')
const router = express.Router()
// Здесь мы определяем маршруты, относящиеся к пользователям
router.get('/', (req, res) => {
res.send('Список пользователей')
})
router.get('/:id', (req, res) => {
res.send(`Пользователь с id = ${req.params.id}`)
})
router.post('/', (req, res) => {
res.send('Создать пользователя')
})
module.exports = router
И подключение в основном файле:
// app.js
const usersRouter = require('./routes/users')
// Все маршруты из usersRouter будут доступны по префиксу /users
app.use('/users', usersRouter)
Теперь вы увидите, как проект становится более управляемым: каждая область ответственности изолирована, и найти нужный маршрут проще.
Маршруты и версии API
Зачем нужны версии
Со временем ваше API будет развиваться:
- вы меняете формат ответа;
- удаляете поля;
- меняете названия ресурсов.
Чтобы не ломать код у существующих клиентов, обычно вводят версии:
/api/v1/users/api/v2/users
Либо используют другие подходы (заголовки, параметры), но версии в пути — один из самых понятных вариантов.
Пример версионирования в маршрутах
// Роутер для первой версии API
const v1 = express.Router()
v1.get('/users', (req, res) => {
// Например, старый формат ответа
res.send({ data: ['user1', 'user2'] })
})
// Роутер для второй версии API
const v2 = express.Router()
v2.get('/users', (req, res) => {
// Новый формат ответа
res.send({ items: ['user1', 'user2'], total: 2 })
})
// Подключаем версии с разными префиксами
app.use('/api/v1', v1)
app.use('/api/v2', v2)
Так клиенты сами выбирают, какую версию использовать, изменяя только путь в запросах.
Middleware и их роль в маршрутах
Что такое middleware
Middleware — это функции, которые выполняются между приходом запроса и тем, как он попадет в обработчик маршрута. Они могут:
- проверять авторизацию;
- логировать запросы;
- парсить тело запроса;
- изменять объект запроса или ответа;
- прерывать обработку и возвращать ответ.
Связь с маршрутами проста: вы можете подключать middleware:
- глобально — для всех маршрутов;
- для группы маршрутов;
- для отдельного маршрута.
Пример middleware для логирования
// Простое middleware логирования
function logRequest(req, res, next) {
console.log(`${req.method} ${req.url}`) // Выводим метод и путь запроса
next() // Переходим к следующему middleware или маршруту
}
// Подключаем для всех маршрутов
app.use(logRequest)
Теперь каждый запрос будет проходить через logRequest, а затем продолжать путь к нужному маршруту.
Middleware на уровне конкретного маршрута
Давайте посмотрим, как повесить middleware только на один маршрут:
// Middleware для проверки "административного" доступа
function requireAdmin(req, res, next) {
if (!req.user || !req.user.isAdmin) {
// Если пользователя нет или он не админ, запрещаем доступ
return res.status(403).send('Доступ запрещен')
}
next() // Если все хорошо, идем дальше к обработчику
}
// Маршрут, защищенный middleware
app.delete('/admin/users/:id', requireAdmin, (req, res) => {
// Этот код выполнится только если requireAdmin вызвал next()
res.send(`Администратор удалил пользователя ${req.params.id}`)
})
Как видите, этот код выполняет проверку прав доступа перед основной логикой маршрута. Это удобно и хорошо структурирует безопасность приложения.
Middleware для групп маршрутов
Вы можете подключить middleware ко всем маршрутам в группе:
// Создаем роутер для /admin
const adminRouter = express.Router()
// Добавляем middleware авторизации ко всем маршрутам админки
adminRouter.use(requireAdmin)
// Теперь все маршруты внутри adminRouter защищены
adminRouter.get('/stats', (req, res) => {
res.send('Административная статистика')
})
adminRouter.delete('/users/:id', (req, res) => {
res.send(`Удален пользователь ${req.params.id} администратором`)
})
// Подключаем группу маршрутов с префиксом /admin
app.use('/admin', adminRouter)
Здесь вы один раз объявляете requireAdmin для роутера, и он будет применяться ко всем маршрутам внутри него. Это облегчает поддержку и уменьшает дублирование кода.
Обработка ошибок и “404 Not Found” для маршрутов
Маршрут по умолчанию для 404
Если ни один из маршрутов не совпал с запросом, сервер обычно отвечает 404 Not Found. Во многих фреймворках вы можете определить обработчик для несуществующих маршрутов.
Пример:
// Здесь мы добавляем обработчик 404 в самом конце списка маршрутов
app.use((req, res) => {
res.status(404).send('Страница не найдена')
})
Важно: такой обработчик нужно размещать после всех объявленных маршрутов, иначе он будет перехватывать запросы раньше.
Централизованная обработка ошибок
Ошибки в обработчиках маршрутов удобно передавать в один общий обработчик. В некоторых фреймворках для этого есть специальный тип middleware.
Пример:
// Маршрут, где может произойти ошибка
app.get('/may-fail', (req, res, next) => {
try {
// Здесь выполняется рискованная операция
throw new Error('Что-то пошло не так')
} catch (err) {
next(err) // Передаем ошибку в следующий обработчик
}
})
// Централизованный обработчик ошибок
app.use((err, req, res, next) => {
console.error('Ошибка в маршруте:', err.message) // Логируем ошибку
res.status(500).send('Внутренняя ошибка сервера')
})
Так вы отделяете логику обработки ошибок от основной логики маршрутов.
Продвинутые техники работы с маршрутами
Генерация ссылок на основе маршрутов
В больших приложениях бывает полезно иметь имена маршрутов (named routes). Тогда вы можете генерировать URL не вручную строками, а на основе этих имен, что уменьшает риск опечаток и облегчает рефакторинг.
Схематичный пример в псевдокоде:
// Регистрируем именованный маршрут
router.name('user.show').get('/users/:id', showUserHandler)
// Позже, в шаблоне или коде, генерируем URL
const url = router.url('user.show', { id: 42 }) // Получаем /users/42
Если вы решите изменить путь, например, на /members/:id, вы меняете его только в определении маршрута, а все места генерации URL продолжат работать.
Ограничение методов и статус 405
Иногда важно явно указывать, какие методы допустимы для конкретного ресурса. Если маршрут существует, но для него не разрешен определенный метод, хорошей практикой считается возвращать 405 Method Not Allowed.
Некоторые роутеры позволяют автоматически генерировать этот ответ, если путь совпал, но метод нет. В других случаях вы можете сделать такую проверку самостоятельно.
Схематичный подход:
// Разрешаем только GET и POST на /users
app.all('/users', (req, res, next) => {
const allowed = ['GET', 'POST']
if (!allowed.includes(req.method)) {
res.set('Allow', allowed.join(', ')) // Указываем допустимые методы
return res.status(405).send('Метод не разрешен')
}
next() // Переходим к конкретному обработчику (GET или POST)
})
Практические рекомендации по проектированию маршрутов
Делайте URL понятными и стабильными
- Используйте читаемые, человекопонятные пути:
/users,/orders,/products/123. - Избегайте внутренних реализационных деталей в URL (типов баз данных, названий таблиц и т.п.).
- Не меняйте существующие маршруты без крайней необходимости — клиенты могут полагаться на них.
Используйте единый стиль
- Единый стиль написания:
kebab-case(/user-profiles),snake_case, но не смешивайте их. - Для REST API — множественное число для коллекций:
/users,/orders. - Не дублируйте информацию: избегайте
/users/user-list.
Разделяйте ответственность по уровням
- Пусть маршруты отвечают только за:
- разбор параметров;
- вызов бизнес‑логики;
- формирование ответа.
- Сложную бизнес‑логику лучше выносить в отдельные функции/сервисы, чтобы маршруты оставались компактными и читабельными.
Продуманно относитесь к безопасности
- Не давайте критичные операции на “простых” маршрутах без проверки прав (
DELETE /users/:idдолжен быть защищен). - Используйте middleware для аутентификации и авторизации на нужных группах маршрутов.
- Ограничивайте доступ по методам и статусам (405 для недопустимых методов).
Заключение
Маршруты (routes) — это механизм сопоставления входящего HTTP‑запроса (метод + путь + дополнительные параметры) с конкретным обработчиком в вашем приложении. От того, насколько аккуратно вы настроите роутинг, зависит:
- читаемость и предсказуемость API;
- удобство поддержки и расширения проекта;
- безопасность и управляемость доступа;
- стабильность для клиентов и фронтенда.
Мы разобрали:
- базовые виды маршрутов (статические, динамические, с параметрами, шаблоны);
- использование HTTP‑методов и RESTful‑подход;
- работу с query‑параметрами и телом запроса;
- влияние порядка объявления маршрутов;
- группировку и модульную структуру роутов;
- версии API, middleware и обработку ошибок;
- практические советы по проектированию.
Используя эти принципы, вы сможете строить маршруты, которые остаются понятными даже в крупных проектах, а изменения в одном месте не ломают работу в другом.
Частозадаваемые технические вопросы по маршрутам
Вопрос 1. Как сделать переадресацию с одного маршрута на другой
Используйте встроенную функцию перенаправления (redirect) фреймворка.
// Старый маршрут
app.get('/old-path', (req, res) => {
// 301 - постоянное перенаправление
res.redirect(301, '/new-path')
})
// Новый маршрут
app.get('/new-path', (req, res) => {
res.send('Новый адрес')
})
Так вы сохраните работоспособность старых ссылок, но будете постепенно переводить пользователей на новый маршрут.
Вопрос 2. Как ограничить частоту запросов к маршруту (rate limiting)
Подключите middleware для rate limiting. В некоторых фреймворках есть готовые решения.
// Псевдокод пример ограничения до 100 запросов в 15 минут
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 100 // максимум 100 запросов с одного IP
})
// Применяем только к маршрутам API
app.use('/api', limiter)
Это помогает защититься от простых DoS атак и чрезмерной нагрузки.
Вопрос 3. Как реализовать локализацию в маршрутах (мультиязычные URL)
Один из вариантов — использовать параметр языка в префиксе маршрута:
// Роутер для локализованных страниц
app.get('/:lang/about', (req, res) => {
const lang = req.params.lang // Например, ru, en, de
// Загружаем нужный язык и отдаем страницу
res.send(`Страница О нас язык ${lang}`)
})
Можно также сделать middleware, которое проверяет язык и подставляет нужные переводы в шаблоны.
Вопрос 4. Как протестировать маршруты автоматически
Используйте инструмент для HTTP‑тестов (например, supertest в Node.js).
// Псевдотест маршрута GET /users
request(app)
.get('/users')
.expect(200) // Проверяем статус
.expect('Content-Type', /json/) // Проверяем заголовок
.end(done) // Завершаем тест
Так вы сможете гарантировать, что изменения в коде не ломают существующие маршруты и их поведение.
Вопрос 5. Как обслуживать статические файлы через маршруты
Большинство фреймворков позволяют указать папку для статических ресурсов.
// Обслуживаем статические файлы из директории public
app.use('/static', express.static('public'))
Теперь файл public/images/logo.png будет доступен по адресу /static/images/logo.png. Это удобно для CSS, JS, изображений и других неизменяемых ресурсов.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев