Перейти к основному содержимому

Express — middleware, маршруты и ошибки

Разработчику

Предварительные знания

Нужен опыт из первой программы на Node.js: app.get, app.post, req.body. Когда эндпоинтов больше пяти, один server.js превращается в "простыню". Ниже — как разложить сервер по папкам, пропустить запрос через middleware, настроить CORS для браузерного фронта и единый формат ошибок.

Склейка с React/Vue/Next: Fullstack. Идеи цепочки middleware и роутеров переносятся на Fastify, Hono и другие фреймворки.

Рекомендуемый порядок изучения

  1. Закрепить мини-API из Первая программа на Node.js с Router и errorHandler.
  2. Подключить клиент на React/Vue/Next и отладить CORS/прокси по Fullstack на JavaScript — API и фронтенд.
  3. При необходимости production — встроенные модули и CLI / деплой.

Как Express обрабатывает один запрос

HTTP-запрос проходит цепочку функций. Middleware может прочитать тело, поставить заголовок, вызвать next() и передать управление дальше или завершить ответ через res.json / res.status — тогда остальные обработчики для этого запроса обычно не вызываются.

Обработчик app.get('/notes', …) — тоже middleware, только последний в своей ветке: он отправляет ответ клиенту.

ОбъектЧто внутри (упрощённо)
reqmethod, url, headers, body, params (:id), query (?page=1)
resstatus(), 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, детали — в лог

Что попробовать

  1. Пакет helmet — базовые заголовки безопасности одной строкой app.use(helmet()).
  2. Разные ответы при NODE_ENV=development и production.
  3. Тесты supertest на GET /health и POST /notesТестирование JavaScript — Vitest и Testing Library.

Связанные материалы


Шаблон middleware-цепочки для командной разработки

Когда над API работают несколько человек, полезно заранее договориться о порядке middleware. Это убирает скрытые баги и ускоряет отладку.


Рекомендуемый порядок подключения

  1. Технические middleware: requestId, логирование.
  2. Безопасность — helmet, cors, лимит тела запроса.
  3. Парсинг: express.json.
  4. Бизнес-маршруты.
  5. notFound для неизвестных путей.
  6. errorHandler для централизованной обработки ошибок.

Единый формат ошибки для фронтенда

ПолеНазначение
codeМашинный код ошибки (validation_error)
messageТекст для пользователя или лога
requestIdИдентификатор запроса для трассировки

Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.