Антишаблоны бэкенда: 7 дорогостоящих ошибок и практические способы их устранения
Узнайте, как выявлять и исправлять семь распространённых ошибок в инженерии бэкенда — от чрезмерно объёмных контроллеров до некорректно обрабатываемых ошибок — прежде чем они приведут к проблемам в производственной среде.
Когда вы только начинаете заниматься разработкой бэкенда, легко предположить, что настоящая сложность заключается в овладении большим количеством инструментов.
Node.js.
Express.
PostgreSQL.
Redis.
Docker.
Очереди сообщений.
Проектирование системы.
Но после выпуска нескольких приложений становится ясно другая истина.
Знание большего количества инструментов автоматически не делает вас лучшим инженером бэкенда.
Большой прогресс достигается благодаря совершению ошибок, пониманию причин их возникновения и предотвращению их повторения.
Ниже приведены семь ошибок в разработке бэкенда, из которых стоит извлечь уроки, а также более эффективный подход к их устранению.
1. Размещение всего в контроллере
Это обычно одна из первых ошибок, с которыми сталкиваются новички.
Эндпоинт может изначально выглядеть так:
app.post("/orders", async (req, res) => {
const { userId, productId, quantity } = req.body;
const user = await db.users.findUnique({
where: { id: userId }
}); if (!user) {
return res.status(404).json({
message: "User not found"
});
} const product = await db.products.findUnique({
where: { id: productId }
}); if (!product) {
return res.status(404).json({
message: "Product not found"
});
} if (product.stock < quantity) {
return res.status(400).json({
message: "Not enough stock"
});
} const order = await db.orders.create({
data: {
userId,
productId,
quantity
}
}); await sendEmail(user.email); return res.status(201).json(order);
});
Это работает.
Но посмотрите, сколько функций теперь выполняет эта одна функция:
- Проверка данных
- Запросы к базе данных
- Бизнес-правила
- Проверка наличия запасов
- Создание заказа
- Работа с электронной почтой
- Ответы HTTP
По мере роста проекта функции, несущие столько обязанностей, превращаются в неконтролируемые блоки логики.
Решение — разделить обязанности.
Request
↓
Controller
↓
Service
↓
Repository
↓
Databas
Задача контроллера — обработка HTTP-запросов.
Логика бизнеса находится в слое сервисов.
Доступ к данным обеспечивается слоем репозиториев.
Это не означает, что маленькому приложению нужно шесть уровней абстракции.
Это означает, что каждая часть системы должна иметь четко определенную задачу.
2. Доверие к фронтенду
Эта ошибка может привести к появлению багов, а еще хуже — к уязвимостям в безопасности.
Предположим, фронтенд отправляет следующий пейлоад:
{
"price": 10,
"quantity": 2
}
Хочется просто использовать цену, отправленную клиентом, для расчёта общей суммы заказа.
Не делайте этого.
Ничто не мешает злонамеренному клиенту отправить:
{
"price": 1,
"quantity": 100
}
Фронтенд находится под контролем пользователя, а не вас.
Ваш бэкенд должен проверять и соблюдать те правила, которые действительно важны.
Например:
const product = await productRepository.findById(
productId
);
const total = product.price * quantity;
Источником истины по вопросам ценообразования должен быть бэкенд, а не клиент.
Та же осторожность должна применяться и к другим областям, на которые клиент может попытаться повлиять:
- Роли пользователей
- Права доступа
- Скидки
- Запасы
- Суммы платежей
- Статус аккаунта
- Владение ресурсами
Рассматривайте фронтенд как инструмент для формирования того, как пользователи воспринимают ваш продукт.
Это не граница безопасности.
3. Некорректная обработка ошибок
В начальных версиях обработка ошибок часто выглядела так:
try {
// something
} catch (error) {
console.log(error);
return res.status(500).json({
message: "Something went wrong"
});
}
В себе использование универсального решения не содержит ничего плохого.
Проблема заключается в том, что от него полагаются во всех ситуациях.
Отсутствие записи пользователя не обязательно является ошибкой уровня 500.
Некорректный запрос не обязательно является ошибкой уровня 500.
Дублирующаяся почта не обязательно является ошибкой уровня 500.
Ваш бэкенд должен различать разные типы сбоев.
Например:
400 → Invalid request
401 → Authentication required
403 → Not allowed
404 → Resource not found
409 → Conflict
422 → Validation failure
500 → Unexpected server error
Выбор конкретных кодов состояния зависит от правил вашего API, но соблюдение последовательности важнее точной схемы.
Структурированные ответы на ошибки также помогают.
Например:
{
"success": false,
"message": "User already exists",
"code": "USER_ALREADY_EXISTS"
}
Благодаря такому формату фронтенд не должен догадываться о причине сбоя.
4. Жесткое задание конфигурации
Эта ошибка кажется безвредной до тех пор, пока вы не попытаетесь развернуть приложение.
Что-то вроде:
const databaseUrl =
"postgresql://user:password@localhost:5432/app";
Или:
const jwtSecret = "my-secret";
Полностью избегайте такого подхода.
Каждая среда, в которой вы работаете, требует собственных настроек.
У вас может быть:
Development
↓
localhost
Staging
↓
staging databaseProduction
↓
production database
Вместо этого используйте конфигурацию, зависящую от среды:
DATABASE_URL=
REDIS_URL=
JWT_SECRET=
PAYMENT_API_KEY=
EMAIL_API_KEY=
И никогда не сохраняйте конфиденциальные данные в системе контроля версий.
Файл .env подходит для локальной разработки, но в производственных средах требуются специальные инструменты для управления конфиденциальной информацией и настройками.
Основной принцип здесь заключается в следующем:
Ваш код не должен быть тесно связан с значениями конфигурации, специфичными для определенной среды.
5. Масштабирование до того, как это действительно необходимо
Это ловушка, в которую попадают многие разработчики в какой-то момент, и на первый взгляд её легко оправдать.
Команда начинает новый проект и сразу же задаётся вопросом:
«Что будет, если завтра появятся 10 миллионов человек?»
Поэтому они решают:
Microservices
Kafka
Redis
Kubernetes
Multiple databases
API Gateway
Event-driven architecture
На бумаге система способна справляться с огромными объёмами работы.
Но на самом деле пользователей всего пять.
Это не надёжная архитектура — это избыточная сложность, которая пока не нужна.
Для большинства проектов разумнее начать с простоты:
Client
↓
Node.js Application
↓
PostgreSQL
И добавлять новые компоненты только тогда, когда есть конкретная причина для этого:
Need caching?
→ Redis
Need background jobs?
→ Queue + WorkerNeed more API capacity?
→ Multiple instances + Load BalancerDatabase becoming a bottleneck?
→ Optimize queries / indexes / architecture
Пусть архитектура развивается вместе с реальными, подтверждёнными потребностями.
Не стоит прибегать к распределённой системе только потому, что в каком-то видео утверждается, что именно это делают «настоящие» старшие инженеры.
6. Блокировка запроса при медленной обработке
Это может серьезно испортить пользовательский опыт работы с API.
Представьте такую схему:
app.post("/order", async (req, res) => {
const order = await createOrder(); await sendEmail(); await generateInvoice(); await notifyWarehouse(); await updateAnalytics(); return res.json(order);
});
Пользователь вынужден ждать, пока все пять шагов будут выполнены по порядку.
Если хотя бы один внешний вызов займет на пять секунд больше времени, вся ответная реакция станет на пять секунд медленнее.
Лучшим решением будет выполнять в рамках запроса только те действия, которые действительно необходимы немедленно.
Все остальное можно поместить в очередь для фоновой обработки.
Client
↓
API
↓
Create Order
↓
Queue Jobs
↓
Response
Далее:
Queue
↓
Worker
├── Send Email
├── Generate Invoice
├── Notification
└── Analytics
Именно в таких ситуациях сервисы вроде BullMQ в сочетании с Redis оказываются крайне полезными.
Но здесь есть еще один важный момент, который легко упустить:
Фоновые задачи должны быть идемпотентными и уметь корректно справляться с сбоями.
Если задача случайно выполняется дважды, возникают следующие риски:
- Выставление клиенту счета более одного раза
- Отправка дублирующихся уведомлений
- Запись дублирующихся записей в базу данных
Простое перенаправление работы в очередь само по себе не решает эту проблему.
Сама задача должна быть разработана с учетом этого факта.
7. Работа без контроля в продакшене
Эта ошибка обычно остается незамеченной до тех пор, пока что-то действительно не сломается.
Представьте, что API в режиме работы вдруг начинает выдавать ошибки.
На первый взгляд сервер кажется в порядке.
Код тоже выглядит нормально.
Но здесь есть один недостаток:
No useful logs
No request IDs
No metrics
No error tracking
No database monitoring
На этом этапе единственным вариантом остается догадки.
Отладка превращается в череду пожиманий плечами:
«Может, Redis вышел из строя?» «Может, база данных работает крайне медленно?» «Может, у поставщика платежных услуг проблемы?»
Для инженера это очень некомфортная ситуация.
Как минимум, необходимы полезные логи.
Например:
{
"level": "error",
"requestId": "req_123",
"route": "/orders",
"userId": "user_456",
"message": "Payment provider timeout"
}
Благодаря таким отчетам становится возможным точно определить, что и где пошло не так.
По мере роста систем объем данных для наблюдения обычно расширяется и включает:
- Логи приложения
- Отслеживание ошибок
- Использование CPU и памяти
- Показатели на уровне базы данных
- Время ответа API
- Размер очереди задач
- Сбои от внешних API
- Точки проверки работоспособности
Система не может оставаться работоспособной, если нет возможности видеть ее поведение.
Более важный урок
Если взглянуть шире, почти все эти проблемы сводятся к одной и той же основной привычке.
Вопрос, который направляет многие решения, как правило, звучит так:
«Работает ли эта функция?»
хотя на самом деле он должен звучать так:
«Легко ли изменять эту функцию, отлаживать её и использовать в производстве?»
Такая смена подхода меняет практически всё в процессе создания программного обеспечения.
Что проверить перед тем, как считать API готовым
Прежде чем считать функцию бэкенда завершённой, полезно пройтись по следующим проверкам.
Код
- У каждого слоя есть чёткая, одна ответственность?
- Можно ли тестировать бизнес-логику изолированно?
- Являются ли контроллеры достаточно лаконичными?
Безопасность
- Проверяется ли вся входящая информация?
- Применяются ли проверки прав на стороне сервера?
- Хранятся ли секретные данные в недоступном месте?
База данных
- Являются ли запросы эффективными?
- Существуют ли необходимые индексы?
- Используются ли транзакции там, где это нужно?
- Есть ли скрытая проблема запросов типа N+1?
Производительность
- Устранены ли избыточные последовательные вызовы?
- Переносится ли тяжелая обработка в фоновый процесс?
- Не улучшит ли кэширование ситуацию здесь?
Надежность
- Каков план действий в случае сбоя внешнего API?
- Настроены ли попытки повторного выполнения разумно?
- Можно ли безопасно запускать фоновые задачи несколько раз?
- Что произойдет, если Redis или база данных станут недоступны?
Эксплуатация
- Возможно ли выяснить, что произошло при сбое?
- Являются ли логи действительно полезными?
- Существуют ли проверки работоспособности?
Идеальная система — не конечная цель.
Но крайне важно знать, что происходит, как только возникают проблемы.
7 ошибок в одном взгляде
| Ошибка | Лучший подход |
|---|---|
| Всё сосредоточено в контроллерах | Разделите обязанности между слоями |
| Доверие к данным с фронтенда | Проверяйте всё на стороне сервера |
| Единый подход к обработке ошибок | Используйте единообразную стратегию обработки ошибок |
| Закодированные в коде секреты | Эффективное управление окружением и конфигурацией |
| Расширение слишком рано | Расширяйте масштабы в ответ на реальные узкие места|
| Медленные операции, блокирующие запросы | Перенесите их в фоновые задачи |
| Отсутствие информации о работе в производственной среде |
Итоги
Существует распространённое мнение, что развитие навыков в качестве разработчика бэкенда означает лишь сбор большего количества инструментов и технологий.
Более точный взгляд заключается в том, что речь идёт о понимании компромиссов.
Должна ли эта операция быть синхронной или асинхронной?
Стоит ли кэшировать эти данные?
Требуется ли для этого запроса индекс?
Должно ли это быть отдельным сервисом?
Каков план на случай сбоя Redis?
Каков план на случай замедления работы базы данных?
Каков план на случай случайного выполнения задачи дважды?
Каков план на случай сбоя внешнего API, от которого мы зависим?
Знание ответов на эти вопросы гораздо важнее, чем простое знание того, как установить ещё один пакет.
Поскольку производственные системы оцениваются не по тому, как они работают, когда всё идёт гладко.
Настоящая инженерия проявляется в тот момент, когда что-то идёт не так.
Каждая сегодня распознанная ошибка — это на один пожар меньше, который придётся тушить позже.
Связанные статьи
- Распространённые ошибки в API-контрактах, разрушающие надёжность фронтенда — Узнайте о десяти часто встречающихся недостатках проектирования API-серверов — от неоднородной структуры ответов до хрупкой системы пагинации — которые подрывают доверие к фронтенду, и способы их устранения.