10. Роутинг и параметры URL

Роутинг — это определение, как приложение отвечает на запросы к определённым URL с определённым HTTP методом.
HTTP методы
Заголовок раздела «HTTP методы»app.get('/users', handler); // Получить списокapp.post('/users', handler); // Создатьapp.put('/users/:id', handler); // Полная заменаapp.patch('/users/:id', handler); // Частичное обновлениеapp.delete('/users/:id', handler);// Удалитьapp.all('/path', handler); // Любой методapp.options('/path', handler); // Preflight CORSПараметры URL
Заголовок раздела «Параметры URL»// :id — обязательный параметрapp.get('/users/:id', (req, res) => { const { id } = req.params; res.json({ id, message: `Пользователь ${id}` });});
// Несколько параметровapp.get('/posts/:year/:month/:slug', (req, res) => { const { year, month, slug } = req.params; res.json({ year, month, slug }); // GET /posts/2024/01/hello-world // → { year: '2024', month: '01', slug: 'hello-world' }});
// Необязательный параметр с ?app.get('/files/:name.:ext?', (req, res) => { const { name, ext = 'txt' } = req.params; res.json({ name, ext });});
// Wildcard *app.get('/public/*', (req, res) => { res.json({ path: req.params[0] }); // GET /public/images/logo.png → { path: 'images/logo.png' }});Query параметры
Заголовок раздела «Query параметры»// GET /users?page=2&limit=10&sort=name&order=asc&role=adminapp.get('/users', (req, res) => { const { page = 1, limit = 10, sort = 'createdAt', order = 'desc', role, search, } = req.query;
// Конвертируем строки в числа const pageNum = parseInt(page, 10); const limitNum = Math.min(parseInt(limit, 10), 100); // не больше 100
const query = { page: pageNum, limit: limitNum, sort, order: order === 'asc' ? 'asc' : 'desc', ...(role && { role }), ...(search && { search }), };
res.json({ query, data: [] });});
// Массивы в query: ?tags[]=js&tags[]=node или ?tags=js&tags=nodeapp.get('/articles', (req, res) => { const tags = [].concat(req.query.tags || []); // всегда массив res.json({ tags });});Express Router — модульный роутинг
Заголовок раздела «Express Router — модульный роутинг»const { Router } = require('express');const router = Router();
// Все роуты здесь будут с префиксом /api/usersrouter.get('/', getUsers);router.get('/:id', getUserById);router.post('/', createUser);router.put('/:id', updateUser);router.delete('/:id', deleteUser);
// Цепочка для одного путиrouter.route('/:id') .get(getUserById) .put(updateUser) .delete(deleteUser);
module.exports = router;const usersRouter = require('./routes/users');const postsRouter = require('./routes/posts');const authRouter = require('./routes/auth');
app.use('/api/users', usersRouter);app.use('/api/posts', postsRouter);app.use('/api/auth', authRouter);Контроллеры
Заголовок раздела «Контроллеры»// controllers/users.js — бизнес-логика отдельно от роутовconst { db } = require('../db');
const getUsers = async (req, res, next) => { try { const { page = 1, limit = 10 } = req.query; const offset = (page - 1) * limit;
const users = await db.users.findMany({ skip: offset, take: parseInt(limit), orderBy: { createdAt: 'desc' }, }); const total = await db.users.count();
res.json({ data: users, pagination: { page: parseInt(page), limit: parseInt(limit), total, pages: Math.ceil(total / limit), }, }); } catch (error) { next(error); // передаём ошибку в error handler }};
const getUserById = async (req, res, next) => { try { const id = parseInt(req.params.id); if (isNaN(id)) { return res.status(400).json({ error: 'Invalid ID' }); }
const user = await db.users.findUnique({ where: { id } }); if (!user) { return res.status(404).json({ error: 'User not found' }); }
res.json(user); } catch (error) { next(error); }};
module.exports = { getUsers, getUserById };Валидация параметров
Заголовок раздела «Валидация параметров»// Ручная валидацияapp.get('/users/:id', (req, res, next) => { const id = parseInt(req.params.id); if (isNaN(id) || id <= 0) { return res.status(400).json({ error: 'ID должен быть положительным числом', }); } req.userId = id; next();}, getUserById);
// Через Zod (рекомендуется)const { z } = require('zod');
const paginationSchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().min(1).max(100).default(10), sort: z.enum(['name', 'email', 'createdAt']).default('createdAt'),});
app.get('/users', (req, res, next) => { const result = paginationSchema.safeParse(req.query); if (!result.success) { return res.status(400).json({ error: 'Некорректные параметры', details: result.error.flatten(), }); } req.query = result.data; next();}, getUsers);Вложенные роуты (nested resources)
Заголовок раздела «Вложенные роуты (nested resources)»// /users/:userId/posts/:postId/commentsconst router = Router({ mergeParams: true });// mergeParams: true — доступ к параметрам родительского роутера
// routes/comments.jsrouter.get('/', async (req, res) => { const { userId, postId } = req.params; // mergeParams нужен! const comments = await db.comments.findMany({ where: { post: { id: postId, authorId: userId } }, }); res.json(comments);});
// app.jsconst commentsRouter = require('./routes/comments');app.use('/users/:userId/posts/:postId/comments', commentsRouter);Практика
Заголовок раздела «Практика»- Создай файл
routes/products.jsс полным CRUD (GET list, GET one, POST, PUT, DELETE) - Реализуй пагинацию через query параметры
?page=1&limit=10 - Добавь валидацию ID параметра (должен быть числом)
- Создай вложенные роуты:
/categories/:catId/products - Реализуй фильтрацию через query:
?minPrice=100&maxPrice=500&category=electronics