Що таке Express router і як його використовувати для модульного маршрутизації?
Express Router - це окремий екземпляр маршрутизатора, створений через express.Router(). Він дозволяє визначати обробники маршрутів в окремих файлах і підключати їх до головного додатку за конкретним префіксом шляху.
Теорія
TL;DR
- Router - це міні-додаток зі своїм стеком middleware, але без
.listen(). Монтуй його, але не запускай самостійно. - Маршрути всередині router є відносними до точки монтування:
router.get('/profile'), підключений до/api/users, стає/api/users/profile. - Менше 10 маршрутів у всьому проекті? Визначай прямо на
app. З'явились кілька файлів або команд? Використовуй Router. app.use('/users', router)- єдиний рядок, що з'єднує обидва.
Швидкий приклад
// routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => res.json({ users: [] })); // GET /api/users
router.get('/:id', (req, res) => res.json({ id: req.params.id })); // GET /api/users/123
module.exports = router;
// app.js
const userRouter = require('./routes/users');
app.use('/api/users', userRouter); // всі маршрути вище стають відносними до /api/usersrouter.get('/') не означає корінь сервера. Це означає "той шлях, куди мене змонтували".
Чому шляхи відносні
Без Router кожен маршрут живе в app.js з абсолютними шляхами. Один ресурс - це 5 маршрутів. Десять ресурсів - 50 маршрутів в одному файлі. Файл швидко добирається до 1000 рядків.
Router створює відокремлену колекцію маршрутів. Коли ти викликаєш app.use('/api/v1/users', userRouter), Express під час кожного запиту відрізає префікс і передає решту шляху до внутрішнього stack matcher цього router. Зміни точку монтування - всі маршрути всередині переїдуть разом.
Коли використовувати
- Менше 10 маршрутів загалом - визначай прямо на
app. Зайві файли не потрібні. - 10+ маршрутів для одного ресурсу - створи
routes/users.js, підключи до/users. - Команда розподілена по доменах - один router на область (
auth,products,admin), кожен у відповідального розробника. - Версіонування API -
v1Routerіv2Router, підключені до/api/v1і/api/v2. Не треба нічого переписувати при виході нової версії. - Middleware для конкретного ресурсу - помісти
requireAuthвсередину router, щоб захистити тільки цей ресурс.
Middleware на рівні Router
Middleware, підключений через router.use(), застосовується тільки до маршрутів у цьому router. Це відрізняється від app.use(), який спрацьовує на кожен вхідний запит.
// routes/admin.js
const router = express.Router();
const { requireAdmin } = require('../middleware/auth');
router.use(requireAdmin); // захищає все нижче цього рядка
router.get('/users', getAllUsers);
router.delete('/users/:id', deleteUser);
module.exports = router;Одне правило щодо порядку: глобальний middleware на кшталт express.json() належить на app, а не всередині кожного router. Якщо помістити парсинг тіла тільки у userRouter, запити до productRouter не матимуть req.body. Я бачив як це ламало API в продакшені.
Як Express обробляє монтування
Коли викликаєш express.Router(), Express створює новий екземпляр Router із власним внутрішнім масивом stack. Кожен маршрут, який ти додаєш, записується як об'єкт Layer у цей stack разом із методом HTTP, патерном шляху та обробником.
Під час запиту Express спочатку проходить по stack головного додатку. Коли зустрічає app.use('/api/users', userRouter), відрізає префікс з URL і передає решту до stack matcher router. Якщо маршрут збігається - запускається обробник. Якщо ні - управління повертається до головного додатку.
Ніякої магії. Просто вкладені стеки.
Поширені помилки
Абсолютні шляхи всередині router
// НЕПРАВИЛЬНО: при монтуванні до /users стає /users/users
router.get('/users', (req, res) => res.json({ users: [] }));
// ПРАВИЛЬНО
router.get('/', (req, res) => res.json({ users: [] }));Шляхи в router завжди відносні до точки монтування. Це найчастіша помилка на code review в Express на junior-рівні.
Забутий module.exports
// users.js визначає все, але забуває цей рядок
module.exports = router;
// app.js
app.use('/users', require('./users')); // TypeError: router is not a functionПовідомлення про помилку виглядає дивно, але виправлення завжди однакове: додай export.
Глобальний middleware після монтування
// НЕПРАВИЛЬНО
app.use('/users', userRouter);
app.use(express.json()); // спрацьовує після монтування, вже запізно
// ПРАВИЛЬНО
app.use(express.json()); // завжди перед маршрутами
app.use('/users', userRouter);Відсутність { mergeParams: true } у вкладених router
Якщо вкладаєш router під параметризований шлях і потрібен доступ до батьківського параметра, дочірній router не побачить його за замовчуванням.
// без цього req.params.userId буде undefined
const orderRouter = express.Router({ mergeParams: true });
orderRouter.get('/', (req, res) => {
res.json({ userId: req.params.userId, orders: [] }); // працює лише з mergeParams
});
router.use('/:userId/orders', orderRouter);Ця помилка часто трапляється у розробників середнього рівня. Якщо req.params виглядає порожнім у дочірньому router - спочатку перевір цю опцію.
Де зустрічається в реальних проектах
- MERN-стек (наприклад, fullstackopen.com) -
routes/api/users.jsпідключений до/apiдля fetch-запитів з фронтенду. - NestJS - використовує Express Router під капотом: кожен
@Controller('users')компілюється в екземпляр Router. - FeathersJS - кожен сервіс монтується як Router за шляхом ресурсу.
- Express boilerplate (наприклад, hagopj13/node-express-boilerplate на GitHub) - один router на домен, підключається в
app.js.
Follow-up питання
Q: Яка різниця між об'єктами app і router?
A: Обидва мають .use(), .get(), .post() та інші методи. Головна відмінність: у router немає .listen(). Не можна запустити сервер з router. Він працює тільки після монтування в app.
Q: Як відрізняється область дії router.use() від app.use()?
A: router.use(fn) застосовується тільки до маршрутів конкретного router. app.use(fn) запускається на кожен вхідний запит перед будь-яким router.
Q: Що таке router.param() і коли його використовувати?
A: router.param('id', fn) запускає callback щоразу, коли :id з'являється в збіжному маршруті, до запуску обробника. Корисно для завантаження юзера з бази один раз і прикріплення до req.user, замість повторення цієї логіки в кожному обробнику.
Q: Чи може router мати власний обробник помилок?
A: Так. Додай middleware з чотирма аргументами в кінці: router.use((err, req, res, next) => { ... }). Він перехоплює помилки з цього router. Якщо викликає next(err), помилка піднімається до обробника рівня app.
Q: Як версіонувати API через router?
A: Створи v1Router і v2Router, підключи до /api/v1 і /api/v2. Старі клієнти продовжують звертатись до v1, нові використовують v2. Існуючі обробники не змінюються.
Приклади
Базовий: один ресурс у окремому файлі
// routes/products.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => res.json({ products: ['laptop', 'phone'] })); // GET /products
router.post('/', (req, res) => res.status(201).json({ id: 1 })); // POST /products
router.get('/:id', (req, res) => res.json({ id: req.params.id })); // GET /products/42
module.exports = router;
// app.js
const express = require('express');
const app = express();
app.use(express.json());
app.use('/products', require('./routes/products'));
app.listen(3000);Три маршрути, один файл. app.js залишається чистим незалежно від кількості обробників всередині products.js.
Середній: захищений API користувачів із middleware
Це ближче до того, що побачиш у реальному MERN-проекті.
// middleware/auth.js
const requireAuth = (req, res, next) => {
const token = req.headers.authorization;
if (!token) return res.status(401).json({ error: 'Unauthorized' });
// тут була б перевірка JWT
next();
};
module.exports = { requireAuth };
// routes/users.js
const express = require('express');
const { requireAuth } = require('../middleware/auth');
const router = express.Router();
router.use(requireAuth); // всі маршрути нижче потребують авторизації
router.get('/', async (req, res) => {
res.json({ users: [{ id: 1, name: 'Alice' }] }); // GET /api/users (захищений)
});
router.post('/', async (req, res) => {
const { name } = req.body;
res.status(201).json({ id: 2, name }); // POST /api/users (захищений)
});
module.exports = router;
// app.js
app.use(express.json());
app.use('/api/users', require('./routes/users'));Перевірка авторизації запускається один раз для всього router. Не треба повторювати її в кожному обробнику.
Просунутий: вкладені router з mergeParams
Вкладені ресурси на кшталт /users/123/orders потребують уважного налаштування.
// routes/orders.js
const express = require('express');
const orderRouter = express.Router({ mergeParams: true }); // успадкувати :userId від батька
orderRouter.get('/', (req, res) => {
// req.params.userId доступний тут завдяки mergeParams
res.json({ userId: req.params.userId, orders: [] });
});
module.exports = orderRouter;
// routes/users.js
const express = require('express');
const router = express.Router();
const orderRouter = require('./orders');
router.param('userId', (req, res, next, id) => {
req.user = { id, name: 'Alice' }; // завантажити юзера до запуску будь-якого обробника
next();
});
router.use('/:userId/orders', orderRouter); // GET /users/123/orders
module.exports = router;Без { mergeParams: true } значення req.params.userId у orderRouter буде undefined. Це поширений баг у багаторівневих REST API.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.