Маршрутизация в Express сопоставляет HTTP-метод и путь с функцией-обработчиком: app.get('/users', handler) отвечает на GET-запросы к /users, а app.post(), app.put() и app.delete() делают то же самое для других методов. Сегмент, начинающийся с двоеточия, как в /users/:id, это параметр маршрута, значение которого попадает в req.params. Чтобы растущее приложение оставалось в порядке, складывайте связанные маршруты в express.Router() и подключайте его под префиксом через app.use('/api', router).
GET /users/2 отвечает {"id":2,"name":"Linus"}. Параметры маршрута всегда строки, поэтому обработчик сравнивает с Number(req.params.id). Отправьте POST /users с телом {"name": "Grace"} и получите 201, затем DELETE /users/1 и GET /users, чтобы увидеть, как меняется список.
express.Router() с префиксом
Router это мини-приложение со своими маршрутами и middleware. Его пути относительные: маршрут /:id внутри router'а, подключённого на /api/books, отвечает на /api/books/:id. В настоящем проекте каждый router живёт в своём файле и заканчивается строкой module.exports = router.
GET /api/books/1 возвращает {"id":1,"title":"Dune"}. router.route('/:id') записывает путь один раз и цепочкой вешает по обработчику на каждый метод; отправьте PUT /api/books/1, и получите {"updated":"1"}. Поменяйте путь подключения на /v2/books, и тот же router будет отвечать там.
Порядок маршрутов: побеждает первое совпадение
Express пробует маршруты в том порядке, в котором вы их зарегистрировали, и останавливается на первом, который отправил ответ. Маршрут с параметром ловит всё на своей позиции, поэтому до фиксированного пути, зарегистрированного после него, дело не доходит:
GET /users/new отвечает {"route":"/users/:id","id":"new"}: маршрут с параметром зарегистрирован первым и забрал запрос. Перенесите маршрут /users/new выше /users/:id и запустите снова, чтобы получить {"route":"/users/new"}. Затем отправьте GET /books, чтобы попасть в обработчик 404 внизу.
Обработчик 404
На несовпавший запрос Express отвечает своей HTML-страницей Cannot GET /books. Для API обычно нужен JSON, поэтому добавьте app.use() без пути самым последним маршрутом, как в примере выше. Он выполняется, только если все маршруты до него передали запрос дальше. Ошибки, выброшенные обработчиками, это отдельный случай со своим обработчиком на четыре аргумента, о нём статья Обработка ошибок в Express.
Пути маршрутов в Express 5
В Express 5 изменился синтаксис путей, а большинство примеров в интернете написаны для Express 4:
| Шаблон | Express 4 | Express 5 |
|---|---|---|
| Любой путь | app.get('*', fn) | app.get('/*splat', fn) |
| Необязательный параметр | /users/:id? | /users{/:id} |
| Шаблон на параметре | /users/:id(\\d+) | Не поддерживается; проверяйте значение в обработчике |
Одиночная * или ? после параметра выбрасывает TypeError при регистрации маршрута, и приложение не запускается.
Частые ошибки
Обработчик 404 зарегистрирован первым. app.use() без пути совпадает со всем, поэтому каждый запрос получает 404. Держите его в самом низу.
Забыли, что префикс отрезается. Внутри router'а, подключённого на /api, req.path не содержит /api. Когда нужен полный путь, используйте req.originalUrl.
Сравнение параметра с числом. req.params.id === 1 всегда false, потому что req.params.id это строка '1'.
Часто задаваемые вопросы
Чем app.use отличается от app.get?
app.get('/users', fn) вызывает fn только для GET-запросов, путь которых ровно /users. app.use('/users', fn) срабатывает для любого метода и любого пути, который начинается с /users, поэтому через него подключают router'ы и middleware.
Как сделать маршруты Express чувствительными к регистру?
По умолчанию маршруты не учитывают регистр, поэтому /Users совпадает с /users. Это отключается через app.set('case sensitive routing', true) или для отдельного router'а через express.Router({ caseSensitive: true }).
Почему req.params пустой во вложенном router'е?
Router видит только параметры своих путей. Если он подключён на /users/:userId/posts, создавайте его через express.Router({ mergeParams: true }), чтобы req.params.userId из родительского пути был доступен.
Стоит ли держать все маршруты в одном файле?
Нет, когда их больше нескольких. Создайте по router'у на ресурс (routes/users.js, routes/orders.js), экспортируйте его через module.exports = router и подключите каждый в главном файле через app.use().