Роутинг в Express
Зачем нужно
Роутинг определяет как приложение отвечает на запрос клиента к конкретному URL и HTTP-методу. Вместо одного огромного if/else для всех путей Express дает декларативный API: app.get('/users', handler), app.post('/users', handler). Правильная организация маршрутов -- ключ к поддерживаемому API.
Где используется
- REST API: маршруты для каждого ресурса (users, posts, comments)
- Веб-приложения: страницы (/, /about, /login)
- Микросервисы: группы маршрутов для доменов
- API-версионирование: /api/v1/users, /api/v2/users
Предпосылки
- Что такое Express — создание Express-приложения, req и res
- http — HTTP-методы и статус-коды
Базовый роутинг
const express = require('express');
const app = express;
// app.METHOD(PATH, HANDLER)
// GET — получить ресурс
app.get('/', (req, res) => {
res.send('Home page');
});
// POST — создать ресурс
app.post('/api/users', (req, res) => {
res.status(201).json({ id: 1, name: req.body.name });
});
// PUT — полная замена ресурса
app.put('/api/users/:id', (req, res) => {
res.json({ id: req.params.id, ...req.body });
});
// PATCH — частичное обновление ресурса
app.patch('/api/users/:id', (req, res) => {
res.json({ id: req.params.id, updated: true });
});
// DELETE — удалить ресурс
app.delete('/api/users/:id', (req, res) => {
res.status(204).end();
});
// ALL — любой HTTP-метод
app.all('/api/secret', (req, res) => {
res.status(403).json({ error: 'Access denied' });
});
app.listen(3000);
Параметры маршрута — req.params
// :param — именованный параметр в URL
// /api/users/42
app.get('/api/users/:id', (req, res) => {
console.log(req.params); // { id: '42' }
console.log(req.params.id); // '42' (всегда строка!)
const id = Number(req.params.id);
res.json({ id });
});
// Несколько параметров: /api/users/42/posts/7
app.get('/api/users/:userId/posts/:postId', (req, res) => {
console.log(req.params);
// { userId: '42', postId: '7' }
res.json(req.params);
});
// Опциональный параметр: /api/users или /api/users/42
app.get('/api/users/:id?', (req, res) => {
if (req.params.id) {
res.json({ user: req.params.id });
} else {
res.json({ users: 'all' });
}
});
Query-параметры — req.query
// Query string: ?key=value&key2=value2
// Автоматически парсится Express
// GET /api/users?page=2&limit=10&sort=name
app.get('/api/users', (req, res) => {
console.log(req.query);
// { page: '2', limit: '10', sort: 'name' }
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const sort = req.query.sort() || 'id';
// Пагинация и сортировка
res.json({ page, limit, sort, data: });
});
// GET /api/search?q=nodejs&tags=backend&tags=javascript
app.get('/api/search', (req, res) => {
console.log(req.query.q); // 'nodejs'
console.log(req.query.tags); // ['backend', 'javascript'] (массив)
res.json({ query: req.query });
});
Тело запроса — req.body
const express = require('express');
const app = express;
// Обязательно подключить middleware для парсинга тела!
app.use(express.json()); // JSON
app.use(express.urlencoded({ extended: true })); // form data
// POST /api/users
// Body: { "name": "Anna", "email": "anna@example.com" }
app.post('/api/users', (req, res) => {
console.log(req.body);
// { name: 'Anna', email: 'anna@example.com' }
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
res.status(201).json({ id: Date.now(), name, email });
});
express.Router — модульный роутинг
Router создает изолированную группу маршрутов, которую можно подключить к приложению с префиксом.
// routes/users.js
const express = require('express');
const router = express.Router;
// Все маршруты относительно точки подключения
// (будет /api/users/...)
// GET /api/users
router.get('/', (req, res) => {
res.json([
{ id: 1, name: 'Anna' },
{ id: 2, name: 'Boris' }
]);
});
// GET /api/users/:id
router.get('/:id', (req, res) => {
res.json({ id: req.params.id, name: 'User' });
});
// POST /api/users
router.post('/', (req, res) => {
res.status(201).json({ id: Date.now(), ...req.body });
});
// PUT /api/users/:id
router.put('/:id', (req, res) => {
res.json({ id: req.params.id, ...req.body });
});
// DELETE /api/users/:id
router.delete('/:id', (req, res) => {
res.status(204).end();
});
module.exports = router;
// app.js
const express = require('express');
const app = express;
const userRoutes = require('./routes/users');
const postRoutes = require('./routes/posts');
const authRoutes = require('./routes/auth');
app.use(express.json());
// Подключение роутеров с префиксами
app.use('/api/users', userRoutes); // /api/users/*
app.use('/api/posts', postRoutes); // /api/posts/*
app.use('/api/auth', authRoutes); // /api/auth/*
app.listen(3000);
Вложенные роутеры
// routes/users.js
const express = require('express');
const router = express.Router;
const postRouter = require('./userPosts');
// Вложенный роутер: /api/users/:userId/posts
router.use('/:userId/posts', postRouter);
router.get('/', (req, res) => { /* ... */ });
router.get('/:id', (req, res) => { /* ... */ });
module.exports = router;
// routes/userPosts.js
const express = require('express');
const router = express.Router({ mergeParams: true }); // ← mergeParams!
// GET /api/users/:userId/posts
router.get('/', (req, res) => {
const { userId } = req.params; // доступен благодаря mergeParams
res.json({ userId, posts: });
});
module.exports = router;
Порядок маршрутов имеет значение
Express проверяет маршруты в порядке определения. Первый совпавший обрабатывает запрос.
// ❌ НЕПРАВИЛЬНЫЙ порядок
app.get('/api/users/:id', (req, res) => {
res.json({ id: req.params.id });
});
app.get('/api/users/me', (req, res) => {
// Этот маршрут НИКОГДА не вызовется!
// '/api/users/me' совпадёт с ':id' выше (id = 'me')
res.json({ user: 'current' });
});
// ✅ ПРАВИЛЬНЫЙ порядок: конкретные маршруты ДО параметризованных
app.get('/api/users/me', (req, res) => {
res.json({ user: 'current' });
});
app.get('/api/users/:id', (req, res) => {
res.json({ id: req.params.id });
});
Regex-маршруты
// Строковые паттерны (Express route syntax)
app.get('/ab?cd', handler); // /acd или /abcd
app.get('/ab+cd', handler); // /abcd, /abbcd, /abbbcd ...
app.get('/ab*cd', handler); // /abcd, /ab123cd, /abXYZcd ...
// Полноценный RegExp
app.get(/.*fly$/, (req, res) => {
// Любой URL, оканчивающийся на 'fly'
// /butterfly, /dragonfly
res.send(`Matched: ${req.url}`);
});
// Параметры с ограничением формата
app.get('/api/users/:id(\\d+)', (req, res) => {
// Только числовые ID: /api/users/42 ✅
// /api/users/abc ❌ (не совпадёт)
res.json({ id: Number(req.params.id) });
});
route — цепочка методов
// Вместо повторения пути:
app.get('/api/books', getBooks);
app.post('/api/books', createBook);
app.put('/api/books', updateBook);
// Используй app.route:
app.route('/api/books')
.get((req, res) => {
res.json([{ id: 1, title: 'Node.js in Action' }]);
})
.post((req, res) => {
res.status(201).json(req.body);
});
app.route('/api/books/:id')
.get((req, res) => {
res.json({ id: req.params.id });
})
.put((req, res) => {
res.json({ id: req.params.id, ...req.body });
})
.delete((req, res) => {
res.status(204).end();
});
Middleware на уровне маршрута
// Middleware только для конкретного маршрута
function validateId(req, res, next) {
const id = Number(req.params.id);
if (isNaN(id) || id < 1) {
return res.status(400).json({ error: 'Invalid ID' });
}
req.numericId = id; // добавляем в req для handler-а
next;
}
function requireAuth(req, res, next) {
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' });
}
next;
}
// Один middleware
app.get('/api/users/:id', validateId, (req, res) => {
res.json({ id: req.numericId });
});
// Несколько middleware (массив или через запятую)
app.delete('/api/users/:id', [requireAuth, validateId], (req, res) => {
res.status(204).end();
});
// Middleware для всех маршрутов роутера
const router = express.Router;
router.use(requireAuth); // все маршруты этого роутера требуют auth
router.get('/profile', (req, res) => { /* ... */ });
router.get('/settings', (req, res) => { /* ... */ });
404 — Not Found
const express = require('express');
const app = express;
// ... все маршруты ...
app.get('/api/users', (req, res) => { /* ... */ });
app.post('/api/users', (req, res) => { /* ... */ });
// 404 handler — ПОСЛЕ всех маршрутов
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Route ${req.method} ${req.url} not found`,
status: 404
});
});
app.listen(3000);
Полный пример: REST API
// routes/products.js
const express = require('express');
const router = express.Router;
let products = [
{ id: 1, name: 'Laptop', price: 999, category: 'electronics' },
{ id: 2, name: 'Book', price: 15, category: 'education' },
{ id: 3, name: 'Phone', price: 699, category: 'electronics' }
];
let nextId = 4;
// GET /api/products?category=electronics&sort=price&order=desc
router.get('/', (req, res) => {
let result = [...products];
// Фильтрация
if (req.query.category) {
result = result.filter(p => p.category === req.query.category);
}
// Сортировка
if (req.query.sort()) {
const field = req.query.sort();
const order = req.query.order === 'desc' ? -1 : 1;
result.sort((a, b) => (a[field] > b[field] ? order : -order));
}
res.json({ count: result.length, data: result });
});
// GET /api/products/:id
router.get('/:id', (req, res) => {
const product = products.find(p => p.id === Number(req.params.id));
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
res.json(product);
});
// POST /api/products
router.post('/', (req, res) => {
const { name, price, category } = req.body;
if (!name || !price) {
return res.status(400).json({ error: 'Name and price are required' });
}
const product = { id: nextId++, name, price: Number(price), category };
products.push(product);
res.status(201).json(product);
});
// PATCH /api/products/:id
router.patch('/:id', (req, res) => {
const product = products.find(p => p.id === Number(req.params.id));
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
const { name, price, category } = req.body;
if (name) product.name = name;
if (price) product.price = Number(price);
if (category) product.category = category;
res.json(product);
});
// DELETE /api/products/:id
router.delete('/:id', (req, res) => {
const index = products.findIndex(p => p.id === Number(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'Product not found' });
}
products.splice(index, 1);
res.status(204).end();
});
module.exports = router;
Частые ошибки
- Порядок маршрутов —
/users/meпосле/users/:idникогда не вызовется,meпопадёт в:id - Забывают
return— послеres.status(400).json(...)код продолжает выполняться, вызывая двойной ответ - req.params.id — строка —
req.params.id === 42всегдаfalse, нужноNumber(req.params.id) - Не подключают express.json() —
req.bodyравенundefinedдля POST/PUT/PATCH - Путают req.params и req.query —
/users/:id= params,/users?id=42= query - Не используют Router — все маршруты в одном файле, проект становится неподдерживаемым
Практика
- Создать CRUD-маршруты для ресурса
/api/productsчерезexpress.Router - Добавить фильтрацию по query-параметрам (category, minPrice, maxPrice)
- Создать вложенный роутер:
/api/users/:userId/orders - Реализовать middleware
validateId, проверяющий что:id— число - Добавить 404-handler для несуществующих маршрутов
Связанные темы
- Что такое Express — основы Express, req и res
- Middleware — middleware цепочка
- http — HTTP-методы и статус-коды