Головна / Статті / Анти-шаблони бекенду: 7 дорогих помилок та їхні практичні рішення

Анти-шаблони бекенду: 7 дорогих помилок та їхні практичні рішення

Дізнайтеся, як виявляти та виправляти сім поширених помилок у роботі бекенд-інжинірингу — від надмірно об’ємних контролерів до неконтрольованих помилок — ще до того, як вони спричинять проблеми у продакшені.

2073 слів

Коли ви тільки починаєте працювати над розробкою бекенду, легко припустити, що справжній виклик полягає у оволодінні більшою кількістю інструментів.

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. Робота без контролю в продакшені

Ця помилка зазвичай залишається непомітною аж до того моменту, коли щось справді зламається.

На перший погляд сервер виглядає нормально.

Код також виглядає без проблем.

Але ось що бракує:

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 або база даних стануть недоступними?

    Операції

    • Чи можна дізнатися, що сталося під час збою?
    • Чи справді корисні журнали подій?
    • Чи існують перевірки стану?
    • Чи можна виміряти продуктивність API?

    Ідеальна система — не кінцева мета.

    Але вкрай важливо знати, що відбувається, як тільки щось йде не так.

    7 помилок у перегляді

    Помилка Кращий підхід
    Усе зібрано в контролерах Розподіліть обов’язки між шарами
    Довіра до даних з фронтенду Перевіряйте все на серверній стороні
    Єдина стратегія обробки помилок Використовуйте послідовну стратегію обробки помилок
    Секрети, закодовані в коді Належне управління середовищем та конфігурацією
    Масштабування занадто рано Масштабуйте у відповідь на реальні обмеження
    Повільні операції, що блокують запити Перенесіть їх у фонові завдання
    Відсутність інформації про роботу в продакшені
    Додати журналування та моніторинг

    Остаточні висновки

    Існує поширена думка, що просування у розвитку як фахівця з бекенду означає лише накопичення більшої кількості інструментів та технологій.

    Більш точний погляд полягає у тому, що справа насправді в розумінні компромісів.

    Чи повинна ця операція бути синхронною чи асинхронною?

    Чи варто кешувати ці дані?

    Чи потребує цей запит індексу?

    Чи слід створити окремий сервіс для цього?

    Який план у разі збою Redis?

    Який план, якщо швидкість роботи бази даних значно знизиться?

    Який план, якщо завдання випадково виконається двічі?

    Який план, якщо зовнішній API, від якого залежимо, перестане працювати?

    Знання відповідей на ці запитання має набагато більше значення, ніж просте знання того, як встановити ще один пакет.

    Адже системи виробництва оцінюються не за тим, як вони працюють, коли все йде гладко.

    Справжня інженерія проявляється саме тоді, коли щось йде не так.

    Кожна помилка, яку ми розуміємо сьогодні, — це один менший „вогонь“ у виробництві, який потрібно буде гасити пізніше.

    Пов’язана література

  • Вибір між Promise.all, Promise.race та послідовними очікуваннями — Дізнайтеся, коли Promise.all() прискорює роботу API Node.js, чому він швидко зазнає невдачі при будь-якому відхиленні, та яка система прийняття рішень допоможе обрати правильний асинхронний патерн.
  • Чому шаблони регулярних виразів з поверненням можуть тихо зависнути ваш сервер — Дізнайтеся, як жадібне зіставлення та катастрофічне повернення можуть перетворити, здавалося б, правильний регулярний вираз на причину зупинки сервера через надмірне використання CPU, та як виявити цей ризик.
  • REST проти GraphQL: Справжні компроміси, які стоять за кожною архітектурою — Пояснює конкретні проблеми, які вирішують REST та GraphQL, їхню внутрішню схему роботи та приховані компроміси, які потрібно врахувати перед вибором одного з них для вашого API.
  • Більше, ніж P95: Вимірювання затримки, яку насправді відчувають ваші користувачі — Чому хороший показник P95 може існувати разом із повільним продуктом, як час очікування в черзі та розповсюдження запитів залишаються непоміченими на панелях керування, та як вимірювання часу для кожного кроку покладає край суперечкам щодо причин затримки.
  • Шість шаблонів інтеграції для надійного з’єднання сервісів Node.js — Дізнайтеся про основні шаблони, які лежать в основі надійних інтеграцій у Node.js: модель запит-відповідь, опитування, webhooks, API key, автентифікація JWT та OAuth, повторні спроби з паузами та мапування даних.