Express — middleware, маршруты и ошибки
Предварительные знания
Нужен опыт из первой программы на Node.js: app.get, app.post, req.body. Когда эндпоинтов больше пяти, один server.js превращается в "простыню". Ниже — как разложить сервер по папкам, пропустить запрос через middleware, настроить CORS для браузерного фронта и единый формат ошибок.
Склейка с React/Vue/Next: Fullstack. Идеи цепочки middleware и роутеров переносятся на Fastify, Hono и другие фреймворки.
Рекомендуемый порядок изучения
- Закрепить мини-API из Первая программа на Node.js с
RouterиerrorHandler. - Подключить клиент на React/Vue/Next и отладить CORS/прокси по Fullstack на JavaScript — API и фронтенд.
- При необходимости production — встроенные модули и CLI / деплой.
Как Express обрабатывает один запрос
HTTP-запрос проходит цепочку функций. Middleware может прочитать тело, поставить заголовок, вызвать next() и передать управление дальше или завершить ответ через res.json / res.status — тогда остальные обработчики для этого запроса обычно не вызываются.
Обработчик app.get('/notes', …) — тоже middleware, только последний в своей ветке: он отправляет ответ клиенту.
| Объект | Что внутри (упрощённо) |
|---|---|
req | method, url, headers, body, params (:id), query (?page=1) |
res | status(), json(), send() — формирование ответа |
next | Переход к следующему middleware; next(err) — прыжок в обработчик ошибок |
Структура папок для учебного API
notes-api/
server.js # создание app, listen
routes/
notes.js # Router для /notes
health.js # GET /health
middleware/
errorHandler.js
notFound.js
store/
memoryStore.js # логика данных (потом — БД)
server.js остаётся тонким: только app.use(...), подключение роутеров, глобальные middleware. Бизнес-логика заметок — в store/ или сервисах.
Router и префиксы
express.Router() — мини-приложение со своими маршрутами. Его монтируют с префиксом:
// routes/health.js
import { Router } from 'express';
const router = Router();
router.get('/', (_req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
export default router;
Разбор:
Router()создаёт изолированный роутер, чтобы не держать все обработчики вserver.js.router.get('/', ...)описывает обработчик для корневого пути внутри этого роутера._reqс подчёркиванием показывает, что параметр запроса в этой функции не используется.res.json(...)отправляет JSON и автоматически выставляетContent-Type: application/json.process.uptime()возвращает время жизни процесса в секундах, поэтому endpoint удобен как health-check.
// server.js
import healthRouter from './routes/health.js';
app.use('/health', healthRouter); // итоговый путь: GET /health
Разбор:
import healthRouter ...подключает модуль с маршрутами как отдельную ответственность.app.use('/health', healthRouter)монтирует роутер на префикс, то есть все пути роутера получают начало/health.- Путь
'/'внутри роутера после монтирования становитсяGET /health. - Такой подход упрощает масштабирование: можно добавить
usersRouter,authRouterпо той же схеме.
Путь в роутере '/' + префикс '/health' = GET /health. Для заметок: app.use('/notes', notesRouter) и внутри роутера router.get('/') → GET /notes.
Плюсы — проще читать, тестировать supertest-ом по модулю, вынести группу в отдельный микросервис позже.
CORS — когда фронт в браузере
Origin — схема + хост + порт. Для браузера http://localhost:5173 и http://localhost:3000 — разные origin, даже на одной машине.
Браузер по правилам безопасности блокирует ответ API, если сервер не вернул заголовки CORS (Access-Control-Allow-Origin и др.). curl и Postman CORS не проверяют — отсюда типичная путаница: "в Postman работает, в React — нет". Сначала повторите URL в терминале — curl / fetch — примеры (раздел про CORS); готовые шаблоны fetch и отладка в браузере — Fetch / axios — типовые запросы; компонент списка с API в React — React — компоненты-рецепты, затем правьте заголовки на Express.
npm install cors
Разбор:
npm install corsдобавляет пакет вdependencies, чтобы middleware был доступен в runtime.- После установки модуль
corsможно подключить черезimport cors from 'cors'. - CORS-конфигурация применяется централизованно через
app.use(...).
import cors from 'cors';
app.use(cors({
origin: ['http://localhost:5173', 'http://127.0.0.1:5173'],
methods: ['GET', 'POST', 'DELETE'],
}));
Разбор:
app.use(cors(...))включает CORS-мидлварь для всех маршрутов, подключённых после этой строки.originзадаёт белый список источников, которым браузер разрешит читать ответы API.methodsформирует политику для preflight-запросаOPTIONS.- Если origin пользователя отсутствует в массиве, браузер заблокирует ответ даже при корректной логике API.
| Параметр | Смысл |
|---|---|
origin | Список адресов фронта, которым разрешён доступ |
methods | Какие HTTP-методы разрешены в preflight |
В production укажите реальный домен фронта. Для cookie с credentials: true нельзя ставить origin: '*' — нужен конкретный домен.
Прокси в Vite — альтернатива: браузер ходит на localhost:5173/api/..., dev-сервер пересылает на :3000. Тогда CORS в API в dev можно не открывать — см. Fullstack на JavaScript — API и фронтенд.
Обработка ошибок
Проблема с async
В async (req, res) => { … } необработанный reject Promise может не попасть в ваш errorHandler. Обёртка передаёт ошибку в next(err):
export const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
router.get('/:id', asyncHandler(async (req, res) => {
const note = await store.find(req.params.id);
if (!note) return res.status(404).json({ error: 'not found' });
res.json(note);
}));
Разбор:
asyncHandlerпринимает исходный обработчик и возвращает новый middleware.Promise.resolve(fn(...))унифицирует поведение для sync/async функций..catch(next)гарантированно пробрасывает исключение в центральныйerrorHandler.req.params.idчитает параметр маршрута из/:id.- Ранний
returnвif (!note)завершает текущий обработчик и не даёт отправить второй ответ.
Центральный error middleware
Подключается после всех маршрутов. Сигнатура ровно четыре аргумента — Express по этому понимает, что это обработчик ошибок:
// middleware/errorHandler.js
export function errorHandler(err, req, res, _next) {
console.error(err);
const status = err.status ?? 500;
res.status(status).json({
error: err.message ?? 'Internal Server Error',
});
}
Разбор:
- Первый параметр
errплюс четыре аргумента — обязательный формат error-middleware в Express. console.error(err)фиксирует полный объект ошибки для логов и диагностики.err.status ?? 500выставляет код по умолчанию, если бизнес-ошибка не задала свой статус.res.status(...).json(...)возвращает единый формат ошибки для фронтенда.
import { notFound } from './middleware/notFound.js';
import { errorHandler } from './middleware/errorHandler.js';
app.use(notFound); // 404 для неизвестных путей
app.use(errorHandler); // всё, что пришло через next(err)
Разбор:
- Порядок критичен: сначала
notFound, потомerrorHandler. notFoundсрабатывает, когда ни один роут не обработал запрос.errorHandlerобрабатывает ошибки изnext(err)и из middleware выше по цепочке.- Если поменять их местами, часть ошибок и 404 будет обрабатываться некорректно.
middleware/notFound.js:
export function notFound(req, res, next) {
res.status(404).json({ error: `Route ${req.method} ${req.url} not found` });
}
Разбор:
- Middleware получает
req.methodиreq.url, чтобы вернуть точный контекст ошибки. res.status(404)задаёт HTTP-статус "маршрут не найден".json(...)формирует предсказуемый ответ, который удобно показывать в UI и логировать.nextздесь не вызывается, потому что ответ уже отправлен.
В бизнес-коде можно бросать осмысленную ошибку:
const err = new Error('text is required');
err.status = 400;
throw err;
Разбор:
new Error(...)создаёт объект ошибки с читаемым сообщением.err.status = 400добавляет HTTP-код для клиентской ошибки валидации.throw errпрерывает обычный поток и передаёт управление вerrorHandler.- Такой паттерн сохраняет бизнес-логику чистой, без дублирования
res.status(...).json(...)в каждом месте.
?? — "если слева null или undefined, возьми справа"; удобно для кода по умолчанию.
Валидация тела запроса
Ручная проверка из Первая программа на Node.js (if (!text)) достаточна для старта. При росте API удобна схема Zod:
npm install zod
Разбор:
- Команда устанавливает библиотеку схемной валидации
zod. - После установки можно описывать контракты запросов декларативно.
- Это уменьшает количество ручных
ifи повышает читаемость API-валидации.
Код ITЗагрузка примера кода…
Разбор:
z.object({...})задаёт точную форму тела запроса.z.string().trim().min(1).max(2000)последовательно нормализует и валидирует полеtext.safeParse(req.body)возвращает объект результата вместо исключения.- При
!parsed.successсервер отдаёт 400 с деталями ошибок схемы. parsed.dataсодержит уже проверенные и приведённые данные, безопасные для бизнес-логики.
safeParse возвращает { success: true, data } или { success: false, error } без выброса исключения — проще отдать 400 клиенту.
Частые ошибки
| Симптом | Причина | Решение |
|---|---|---|
| CORS только в Postman | Браузерная политика | cors или прокси Vite |
Cannot set headers after they are sent | Дважды res.json или нет return после ошибки | После res.status(400).json(...) — return |
| 404 на всех путях | Router без префикса или лишний префикс | Сверить app.use('/notes', router) и пути внутри |
| Stack trace у клиента | В production в JSON только error, детали — в лог |
Что попробовать
- Пакет
helmet— базовые заголовки безопасности одной строкойapp.use(helmet()). - Разные ответы при
NODE_ENV=developmentиproduction. - Тесты supertest на
GET /healthиPOST /notes— Тестирование JavaScript — Vitest и Testing Library.
Связанные материалы
- Первая программа на Node.js
- Fullstack — API и фронт
- Node.js — серверный JavaScript
- React · галерея компонентов (Lab)
- Тестирование Vitest
Шаблон middleware-цепочки для командной разработки
Когда над API работают несколько человек, полезно заранее договориться о порядке middleware. Это убирает скрытые баги и ускоряет отладку.
Рекомендуемый порядок подключения
- Технические middleware:
requestId, логирование. - Безопасность —
helmet,cors, лимит тела запроса. - Парсинг:
express.json. - Бизнес-маршруты.
notFoundдля неизвестных путей.errorHandlerдля централизованной обработки ошибок.
Единый формат ошибки для фронтенда
| Поле | Назначение |
|---|---|
code | Машинный код ошибки (validation_error) |
message | Текст для пользователя или лога |
requestId | Идентификатор запроса для трассировки |
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.