Модуль 2
Маршрутизация (Routing)
- HTTP-методы
- app.get/post/put/delete
- Параметры
- Query
- express.Router
- Группировка
#2.1 HTTP-методы
Протокол HTTP определяет методы (verbs), описывающие действие над ресурсом. Express предоставляет методы для каждого из них. В REST API методы соответствуют CRUD-операциям:
- GET — получить данные (Read). Не изменяет состояние сервера
- POST — создать новый ресурс (Create). Данные в теле запроса
- PUT — полностью заменить ресурс (Update). Отправляется весь объект
- PATCH — частично обновить ресурс. Отправляются только изменённые поля
- DELETE — удалить ресурс (Delete)
const express = require('express');
const app = express();
// Парсинг JSON из тела запроса
app.use(express.json());
// GET — получить всех пользователей
app.get('/api/users', (req, res) => {
res.json([{ id: 1, name: 'Алексей' }]);
});
// POST — создать пользователя
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
res.status(201).json({ id: 2, name, email });
});
// PUT — заменить пользователя полностью
app.put('/api/users/:id', (req, res) => {
const { id } = req.params;
const { name, email } = req.body;
res.json({ id, name, email });
});
// PATCH — обновить частично
app.patch('/api/users/:id', (req, res) => {
const { id } = req.params;
res.json({ id, ...req.body });
});
// DELETE — удалить
app.delete('/api/users/:id', (req, res) => {
res.status(204).send(); // 204 No Content
});Соответствие методов и CRUD — конвенция REST, и её стоит соблюдать: GET только читает (не меняет состояние, кешируется), POST создаёт, PUT заменяет ресурс целиком, PATCH правит частично, DELETE удаляет. Важна идемпотентность: повторный GET, PUT или DELETE оставляет данные на сервере в том же состоянии (хотя ответ может отличаться: второй DELETE вернёт 404), а повторный POST создаёт ещё одну запись — поэтому для регистрации и оплаты используют POST.
#2.2 Параметры маршрута (Route Params)
Параметры маршрута — динамические части URL, обозначенные двоеточием (:). Они доступны через req.params. Используются для идентификации конкретного ресурса: пользователя по ID, статьи по slug, категории по имени.
// Один параметр
app.get('/api/users/:id', (req, res) => {
const { id } = req.params; // id = "42"
res.json({ userId: id });
});
// GET /api/users/42 → { userId: "42" }
// Несколько параметров
app.get('/api/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.json({ userId, postId });
});
// GET /api/users/5/posts/12 → { userId: "5", postId: "12" }
// Опциональный сегмент — в фигурных скобках (Express 5)
app.get('/api/posts/:year{/:month}', (req, res) => {
const { year, month } = req.params;
res.json({ year, month: month || 'all' });
});
// GET /api/posts/2025 → { year: "2025", month: "all" }
// GET /api/posts/2025/03 → { year: "2025", month: "03" }Важно: параметры всегда строки. Для числовых операций требуется преобразование: Number(req.params.id) или parseInt(req.params.id, 10).
#2.3 Query-параметры
Query-параметры — часть URL после знака ?. Используются для фильтрации, сортировки, пагинации, поиска. Доступны через req.query. В отличие от параметров маршрута, query-параметры необязательны.
// GET /api/users?page=2&limit=10&sort=name&order=asc
app.get('/api/users', (req, res) => {
const {
page = 1, // значение по умолчанию
limit = 10,
sort = 'id',
order = 'asc',
search = ''
} = req.query;
console.log(page); // "2" (строка!)
console.log(limit); // "10"
// Преобразование типов
const pageNum = parseInt(page, 10);
const limitNum = parseInt(limit, 10);
res.json({
page: pageNum,
limit: limitNum,
sort,
order,
search
});
});
// GET /api/products?category=electronics&minPrice=100&maxPrice=500
app.get('/api/products', (req, res) => {
const { category, minPrice, maxPrice } = req.query;
// Фильтрация по параметрам...
res.json({ category, minPrice, maxPrice });
});Query-параметры (после ?) — для необязательных уточнений: фильтров, сортировки, пагинации, поиска. Их читают из req.query, удобно задавая значения по умолчанию в деструктуризации ({ page = 1 } = req.query). ⚠️ Как и параметры маршрута, все значения query — строки, для чисел нужен parseInt/Number. Повторяющийся ключ (?tag=a&tag=b) Express отдаёт массивом, поэтому при строгой обработке проверяйте тип значения.
#2.4 express.Router()
По мере роста приложения единый файл со всеми маршрутами становится громоздким. express.Router() создаёт модульный обработчик маршрутов — мини-приложение, которое можно подключить к основному. Каждый ресурс (users, posts, products) выносится в отдельный файл.
// routes/users.js
const express = require('express');
const router = express.Router();
// Все маршруты относительно точки монтирования
router.get('/', (req, res) => {
res.json([{ id: 1, name: 'Алексей' }]);
});
router.get('/:id', (req, res) => {
res.json({ id: req.params.id });
});
router.post('/', (req, res) => {
res.status(201).json(req.body);
});
router.put('/:id', (req, res) => {
res.json({ id: req.params.id, ...req.body });
});
router.delete('/:id', (req, res) => {
res.status(204).send();
});
module.exports = router;
// index.js (или app.js)
const express = require('express');
const app = express();
const usersRouter = require('./routes/users');
const postsRouter = require('./routes/posts'); // пишется по образцу users.js — без этого файла Node остановится с ошибкой Cannot find module
app.use(express.json());
// Монтирование роутеров
app.use('/api/users', usersRouter); // /api/users + /
app.use('/api/posts', postsRouter); // /api/posts + /
app.listen(3000);Теперь router.get('/') в users.js обрабатывает GET /api/users, а router.get('/:id') — GET /api/users/42. Префикс задаётся при монтировании через app.use().
express.Router() — это «мини-приложение»: маршруты ресурса описывают относительно корня ('/', '/:id') в отдельном файле, экспортируют router и монтируют через app.use('/api/users', usersRouter). Так каждый ресурс описан в своём файле, а префикс задаётся в одном месте — стандартная организация реальных Express-приложений (так устроено и демо курса).
#2.5 Цепочка обработчиков (Route Chaining)
Метод app.route() позволяет определить несколько HTTP-методов для одного пути в цепочке. Это удобно, когда один URL принимает разные методы.
// Цепочка для одного пути
app.route('/api/books')
.get((req, res) => {
res.json([{ 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).send();
});app.route() группирует несколько методов одного пути в цепочку, чтобы не повторять URL в каждом app.get/app.post. Приём влияет только на читаемость — поведение то же, что у отдельных вызовов.
#2.6 Обработка 404
Если ни один маршрут не совпал с запросом, Express по умолчанию отправляет HTML-страницу «Cannot GET /path». Для API лучше вернуть JSON-ответ. Добавьте обработчик 404 после всех маршрутов — он сработает, если ни один предыдущий не ответил.
// Все маршруты выше...
app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);
// 404 — маршрут не найден (после всех маршрутов!)
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Маршрут ${req.method} ${req.originalUrl} не найден`,
statusCode: 404
});
});
app.listen(3000);Обработчик 404 — это middleware без пути (app.use с одним колбэком), и он обязан стоять после всех маршрутов: Express дойдёт до него, только если ни один маршрут выше не ответил. Отсюда правило порядка в Express: сначала парсеры и общие middleware, затем маршруты, затем 404, и в самом конце — обработчик ошибок (модуль 9).
#2.7 Практика
#Хранилище в памяти: полный пример CRUD
До сих пор все примеры модуля были заглушками: они возвращали жёстко заданный массив, копию req.body или { id: req.params.id }, но ничего не хранили. Настоящее API должно хранить данные и работать с ними. На время обучения простейшее хранилище — массив объектов в памяти процесса: он сбрасывается при перезапуске сервера, но для практики этого достаточно (постоянную базу данных подключим в модуле 6). Ниже — полный роутер, который добавляет, ищет, обновляет и удаляет записи в таком хранилище; сохраните его в routes/users.js и смонтируйте, как показано в разделе 2.4.
// routes/users.js
const express = require('express');
const router = express.Router();
// Хранилище в памяти: массив объектов и счётчик следующего id
const users = [
{ id: 1, name: 'Алексей', age: 30 },
{ id: 2, name: 'Мария', age: 25 }
];
let nextId = 3;
// GET / — список всех
router.get('/', (req, res) => {
res.json(users);
});
// GET /:id — одна запись по id (404, если не найдена)
router.get('/:id', (req, res) => {
const id = Number(req.params.id); // params — строка, приводим к числу
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({ error: 'Пользователь не найден' });
}
res.json(user);
});
// POST / — создать: генерируем id, отвечаем 201
router.post('/', (req, res) => {
const user = { id: nextId++, name: req.body.name, age: Number(req.body.age) };
users.push(user);
res.status(201).json(user);
});
// PUT /:id — заменить запись целиком (404, если не найдена)
router.put('/:id', (req, res) => {
const id = Number(req.params.id);
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({ error: 'Пользователь не найден' });
}
user.name = req.body.name;
user.age = Number(req.body.age);
res.json(user);
});
// DELETE /:id — удалить через splice (404, если не найдена)
router.delete('/:id', (req, res) => {
const id = Number(req.params.id);
const index = users.findIndex(u => u.id === id);
if (index === -1) {
return res.status(404).json({ error: 'Пользователь не найден' });
}
users.splice(index, 1);
res.status(204).send();
});
module.exports = router;Разберём ключевые приёмы. Одну запись ищем через users.find(u => u.id === id): если find вернул undefined, отвечаем 404 непосредственно из обработчика — это 404 для конкретного ресурса, в отличие от глобального 404 из раздела 2.6, который перехватывает запросы к несуществующим маршрутам. Новый id выдаём счётчиком nextId++ и возвращаем статус 201 Created. Для удаления находим позицию через users.findIndex(...) и вырезаем элемент методом users.splice(index, 1). Важен return перед res.status(404): без него код продолжит выполняться и попытается отправить второй ответ — Express выдаст ошибку «Cannot set headers after they are sent».
#Как проверить POST/PUT/DELETE
Адресная строка браузера отправляет только GET: переходом по ссылке нельзя послать POST, PUT, PATCH или DELETE с телом. Чтобы проверить не-GET маршруты (иначе половина CRUD-API остаётся непроверенной), нужен отдельный HTTP-клиент. Разберём три способа: команду curl в терминале, графическое расширение в VS Code и функцию fetch из консоли браузера.
Первый способ — curl (утилита командной строки, отправляющая запрос любым методом):
# создать пользователя: POST с JSON-телом
curl -X POST http://localhost:3000/api/users -H "Content-Type: application/json" -d "{\"name\":\"Иван\",\"age\":40}"
# заменить пользователя целиком: PUT
curl -X PUT http://localhost:3000/api/users/3 -H "Content-Type: application/json" -d "{\"name\":\"Иван\",\"age\":41}"
# удалить: DELETE (тело не нужно)
curl -X DELETE http://localhost:3000/api/users/3Второй способ — графический клиент в VS Code, самый удобный на Windows. Расширение Thunder Client даёт панель, в которой выбирают метод, вводят URL, открывают вкладку Body → JSON и нажимают кнопку Send. Расширение REST Client позволяет записать запрос текстом в файле .http и запустить его ссылкой Send Request над этим запросом. Оба расширения устанавливаются из вкладки Extensions и не требуют командной строки; ту же задачу решает и отдельное приложение Postman.
Третий способ — функция fetch из консоли браузера. Откройте в браузере любой GET-маршрут своего сервера (например, http://localhost:3000/api/users), нажмите F12 → вкладка Console и на этой же вкладке выполните (так запрос будет с того же адреса, без блокировки CORS):
// отправить POST прямо из консоли браузера
fetch('http://localhost:3000/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Иван', age: 40 })
})
.then(res => res.json())
.then(console.log);#Задание 2.1 — REST API книжного магазина
- Создайте routes/books.js с express.Router()
- GET /api/books — список книг (массив в памяти)
- GET /api/books/:id — одна книга по ID
- POST /api/books — добавить книгу
- PUT /api/books/:id — обновить книгу
- DELETE /api/books/:id — удалить книгу
- GET /api/books?author=Толстой&genre=роман — фильтрация
- GET /api/books?page=1&limit=5 — пагинация
- Обработка 404 для несуществующих маршрутов
#Итоги модуля 2
- HTTP-методы соответствуют CRUD: GET читает, POST создаёт, PUT заменяет ресурс целиком, PATCH правит частично, DELETE удаляет; GET, PUT и DELETE идемпотентны, а повторный POST создаёт ещё одну запись.
- Статус-коды — часть контракта API: 200 для GET, 201 Created после POST, 204 No Content для DELETE; без app.use(express.json()) тело POST/PUT не попадёт в req.body.
- Параметры маршрута объявляются двоеточием (/api/users/:id) и читаются из req.params; поддерживаются несколько параметров в одном пути и опциональные сегменты — в Express 5 их берут в фигурные скобки (/api/posts/:year{/:month}).
- Все значения req.params и req.query — строки; для числовых операций обязательны Number или parseInt.
- Порядок маршрутов критичен: Express идёт сверху вниз и берёт первый совпавший, поэтому статические пути (/users/new) объявляют выше динамических (/users/:id).
- Query-параметры после знака ? — для необязательных уточнений: фильтров, сортировки, пагинации, поиска; читаются из req.query, значения по умолчанию удобно задавать в деструктуризации.
- Повторяющийся query-ключ (?tag=a&tag=b) Express отдаёт массивом — при строгой обработке проверяйте тип значения.
- express.Router() — мини-приложение для одного ресурса: маршруты описываются относительно корня в отдельном файле, экспортируются и монтируются через app.use('/api/users', usersRouter); префикс задаётся в одном месте.
- app.route(путь) группирует несколько методов одного URL в цепочку .get().post() — чистая косметика для читаемости, поведение не меняется.
- Обработчик 404 — app.use с одним колбэком после всех маршрутов: срабатывает, только если никто выше не ответил, и для API возвращает JSON вместо HTML-страницы Cannot GET; общий порядок: парсеры → маршруты → 404 → обработчик ошибок.
- Хранилище в памяти для практики — массив объектов: поиск find/findIndex по id с приведением Number(req.params.id), добавление push с генерацией id и статусом 201, обновление найденной записи, удаление splice; если элемента нет — верните 404 непосредственно из обработчика (404 для конкретного ресурса в дополнение к глобальному) и обязательно поставьте return, чтобы ответ не был отправлен дважды.
- Не-GET запросы (POST/PUT/PATCH/DELETE с телом) браузер из адресной строки не отправляет — проверяйте их HTTP-клиентом: командой curl, расширениями Thunder Client или REST Client в VS Code либо fetch из консоли; в Windows PowerShell curl — это псевдоним Invoke-WebRequest, поэтому вызывайте curl.exe или используйте графический клиент.
#Домашнее задание
Постройте с нуля API кулинарной книги — те же приёмы маршрутизации, но на другом ресурсе.
- Создайте проект recipe-api и роутер routes/recipes.js через express.Router(), смонтируйте его на /api/recipes.
- Реализуйте CRUD с корректными статус-кодами: GET /api/recipes (список из массива в памяти), GET /api/recipes/:id (404, если нет), POST (201), PUT /api/recipes/:id, DELETE /api/recipes/:id (204).
- Добавьте фильтрацию через query: /api/recipes?cuisine=итальянская&maxTime=30 (кухня и максимальное время приготовления) со значениями по умолчанию.
- Добавьте статический маршрут GET /api/recipes/popular (три рецепта с лучшим рейтингом) и убедитесь, что он объявлен раньше /:id — запрос /popular не должен попадать в параметр id.
- Перепишите маршруты одного пути цепочками router.route('/') и router.route('/:id').
- (со звёздочкой) Реализуйте вложенный ресурс: GET /api/recipes/:id/ingredients и POST /api/recipes/:id/ingredients, а также JSON-обработчик 404 с полями error, method и url.