Роутинг, обработка ошибок и валидация
«Где вы обрабатываете ошибки в Express?» — вопрос, по ответу на который сразу видно, писал ли человек прод или только туториалы.
В Express 4 асинхронные ошибки не попадают в обработчик ошибок сами:
throwвнутриasync-функции превращается в отклонённый промис, о котором фреймворк ничего не знает.
Что проверяет интервьюер
Три вещи: понимаете ли вы, что обработка ошибок должна быть централизованной; знаете ли ловушку с async-обработчиками; и относитесь ли вы к входным данным как к недоверенным. Последнее — вопрос уже про безопасность, и на senior-позициях он весит больше всего.
Роутинг: порядок и параметры
const router = require('express').Router();
router.get('/orders/new', renderForm); // конкретный путь — раньше
router.get('/orders/:id', getOrder); // иначе ':id' поймает 'new'
router.param('id', (req, res, next, id) => {
if (!/^\d+$/.test(id)) return next(new BadRequestError('id должен быть числом'));
req.orderId = Number(id);
next();
});
app.use('/api/v1', router); // весь роутер под общим префиксом
Маршруты проверяются сверху вниз до первого совпадения, поэтому статические пути всегда регистрируют раньше параметрических. И маленькая деталь, которая нравится интервьюерам: req.params.id — это всегда строка, приведение типа и проверка формата на вас.
Централизованный обработчик ошибок
class AppError extends Error {
constructor(status, code, message) {
super(message);
this.status = status;
this.code = code;
}
}
// регистрируется последним, после всех маршрутов
app.use((err, req, res, next) => {
const status = err.status || 500;
logger.error({ err, requestId: req.id, url: req.originalUrl }, 'request failed');
res.status(status).json({
error: {
code: err.code || 'internal_error',
// наружу отдаём текст только для «ожидаемых» ошибок
message: status < 500 ? err.message : 'Внутренняя ошибка сервера',
},
});
});
Правило, которое стоит проговорить: детали 500-й ошибки наружу не отдаются никогда. Стектрейс и сообщение драйвера базы уходят в лог, клиент получает нейтральный текст и код ошибки. Иначе вы бесплатно рассказываете атакующему про структуру базы и версии библиотек.
Ловушка с async-обработчиками
// Express 4: ошибка НЕ дойдёт до errorHandler, будет unhandled rejection
app.get('/orders/:id', async (req, res) => {
const order = await db.getOrder(req.params.id); // если упадёт — тишина
res.json(order);
});
// вариант 1: try/catch и next(err) вручную
app.get('/orders/:id', async (req, res, next) => {
try {
res.json(await db.getOrder(req.params.id));
} catch (err) {
next(err);
}
});
// вариант 2: обёртка, чтобы не писать try/catch в каждом маршруте
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/orders/:id', asyncHandler(async (req, res) => {
res.json(await db.getOrder(req.params.id));
}));
В Express 5 эту проблему решили: отклонённый промис из обработчика автоматически уходит в next(err). Но проектов на четвёртой версии по-прежнему подавляющее большинство, поэтому знание про asyncHandler — практически обязательное.
Валидация входных данных
Валидировать нужно всё, что приходит извне: body, query, params, заголовки. Ключевой принцип — белый список: описываем разрешённые поля и типы, всё остальное отбрасываем. Иначе легко получить mass assignment, когда клиент присылает {"role": "admin"}, а код честно пишет это в базу.
const { z } = require('zod');
const createUserSchema = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(150),
role: z.enum(['user', 'editor']).default('user'), // 'admin' не пройдёт
});
const validate = (schema) => (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return next(new AppError(400, 'validation_failed', result.error.message));
}
req.body = result.data; // дальше идут только очищенные данные
next();
};
app.post('/users', validate(createUserSchema), asyncHandler(createUser));
Библиотека может быть любой — zod, joi, express-validator, ajv. Важна не она, а схема ответа: валидация оформлена как middleware, ошибки идут в общий обработчик, а в бизнес-логику попадают уже проверенные и приведённые к типам данные. Про коды: 400 — тело не разобрано или структура неверна, 422 — структура верна, но значения не проходят бизнес-правила; строгого стандарта нет, важно быть последовательным.
Типичные ошибки кандидатов
- Обрабатывают ошибки
try/catchв каждом маршруте и не знают про централизованный обработчик. - Не знают ловушку с
asyncв Express 4. - Отдают клиенту
err.stackили сообщение драйвера БД. - Валидируют только
body, забывая проqueryиparams. - Пишут
User.create(req.body)напрямую — прямой путь к mass assignment.
Как ответить кратко
Маршруты проверяются сверху вниз, поэтому конкретные пути регистрируют раньше параметрических. Ошибки собираются в одном middleware с четырьмя аргументами, зарегистрированном последним: он логирует детали и отдаёт клиенту безопасный ответ, никогда не показывая стектрейс на 500. В Express 4 ошибка из
async-обработчика сама туда не попадёт — нуженtry/catchсnext(err)или обёрткаasyncHandler; в Express 5 это уже работает из коробки. Входные данные валидируются схемой в отдельном middleware по принципу белого списка, чтобы в логику не проходили лишние поля.