Галоўная / Артыкулы / Анті-шаблоны бэкенду: 7 дорагаючых памялаў і ўсуненне іх у практыцы

Анті-шаблоны бэкенду: 7 дорагаючых памялаў і ўсуненне іх у практыцы

Дазвольце дазнацься, як выявіць і паспрацаваць з семью распашытымі галатамі інжынерыі бэкенду — ад занадта большых кантролераў да неконтрольваных адказоў — прычым яны не спрычыняюць проблем у рэальных умовах.

2073 слоў

Калі вы толькі пачынаеце працаваць над разработкай бэкенду, лёгка ўявіць, што справжній вызов — адкулькі лепшае опанаванне інструментаў.

Node.js.

Express.

PostgreSQL.

Redis.

Docker.

Message queues.

System design.

Але пасля стварэння некалькіх прыемлівых застосоў стае ясна іншая правда.

Знанне большай колькасці інструментаў не прыводзіць автаматычна да таго, што вы станете кращым інжынерам бэкенду.

Большая частка рэальнага прыросту вырабляецца праз дзеянне помылак, з’ясаванне прычын іх настанку і стараннае прабава, каб яны больш не трапляліся.

Ёсць семь помылак у роботе з бэкендам, якія варта вывучыць, а таксама падходы, якія работаюць краща.

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

    Аператывыя задачы

    • Чы можна дазнаться, што сталося, калі ўсё зламаецца?
    • Чы логі дзейсна корыстныя?
    • Чы існуюць перагляды стану?
  • Чы можна вымерваець каркаснасць API?
  • Цель — не абсалютна бездзеянка системы.

    Але важна знаты, што адбываецца, калі ўсё йде не так.

    7 глухіх памылак за момент

    Іспользуйце аднаковую стратэгію обработкі памылак Здыйсняйце скейлінг у залежнасці ад рэальных перашкод
    Памылка Лепшы падход
    Усё складзенае ў кантролеры Раздзеліце абавясанні межа ў роўнах
    Доверлівасць да дадзеных з фронтэнду Працаваць з усім са стороны сервера
    Едынакавая обработка памылак
    Захаваныя у коде секрэты Правільна адарожнеча среды і налашоўкаў
    Скейлінг занадта рана
    Павільныя задачы, якія блакуюць запиты Перадаеце іх у фонавыя задачы
    Няма інформацыі пра роботу ў продакшэне
    Дадзіце логаванне і монітарынг

    Заключныя вывары

    Існуе паўзучая думка, што прыступленне да вышэйшага рангу як разработчыка бэкенду значыць толькі збір большай колькасці інструментаў і тэхналогій.

    Болей точны пагляд — гэта насамперад зрозумеўце компрэсаў.

    Чы гэтае аперацыя павінна быць сінхронной чы асінхронной?

    Чы цяперашняе даннё карыцься для кэшавання?

    Чы гэтая запитаўка патрабуе індекса?

    Чы гэта павінна стаць самастоятным сервісам?

    Какі план, якщо Redis зупініцца?

    Какі план, якщо база дадзеных сповольніцца?

    Какі план, якщо задача випадкова будзе выконвана два разы?

    Какі план, якщо зовнішняя API, на якую паслугваемся, зламаецца?

    Знанне адказоў на гэтыя запытанні мае набліжна большое значэнне, чым проста ведомасць, як установіць ўтолькі што іншы пакет.

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

    Настоямы інжынерныя навыкі практыкуюцца тады, калі ўтвараюцься проблемы.

    І кожная памылка, якую разумеюць сёння, — это адна меншая проблема ў вырабатцы, якую трэба будзе рашыць пазней.

    Спадневаная літэратура

  • Выбір между Promise.all, Promise.race і парадигмай Sequential Awaits — Дазвольце дазнацца, калі Promise.all() прыяўляе швальнасць API Node.js, чаму ён быстра збіваецца праз будзь-яю адмову, і які критэрыя можна выкарыстоўваць для выбору правильнага асінхроннага падходу.
  • Чаму патэрны regex з алгорытмам backtracking можу таямна заставіць ваш сервер у стане зависання — Дазвольце дазнацца, як жаданнеяды падбіранне і катастрофічны алгорытм backtracking можу ператварыць, здавалася бы, правільны патэрн regex у прычыну перашкод у роботе сервера, якая выкарыстоўвае весь процэсар, і як выявіць такую апасоŭненне.
  • REST vs GraphQL: The Real Trade-Offs Behind Each Architecture — Якраз тут адмініструючыя працавае паспраўляюць спецыфічныя проблемы, якія рашаюць REST і GraphQL, их внутрэшняія механізмы і супакрытыя компромісы, якія трэба взважыць пры выборе аднаго з іх для вашай API.
  • Beyond P95: Measuring the Latency Your Users Actually Experience — Чаму здаровы показнік P95 можа існаваць разам з повольным продуктом, як час чакання ў черзі і распространэнне запытоў заховваюцца ад панелей керування, і як відлік часу за кожным крокам паканчыць гэру звынувачэнняў у повалі швайнасці.
  • Шэсьць шаблонаў інтеграцыі для надзеяного з’ёднання служб Node.js — Дзеўедзіце пра основныя шаблоны, якія лежачы ў падазе надзеяных інтеграцый у Node.js: модель запыт-адказ, поллінг, вебхукі, ключі API, аутэнтыкацыя JWT і OAuth, павторныя спробы з адкладаннем, а таксама картаванне дадзэнняў.