Strona główna / Artykuły / Antypaternale backendu: 7 kosztownych błędów i praktyczne sposoby ich naprawy

Antypaternale backendu: 7 kosztownych błędów i praktyczne sposoby ich naprawy

Dowiedz się, jak wykrywać i naprawiać siedem powszechnych błędów w inżynierii backendu – od przeciążonych kontrolerów po błędy nierozwiązane – zanim spowodują one problemy w produkcji.

2073 słów

Gdy dopiero zaczynasz pracę nad rozwojem backendu, łatwo jest założyć, że prawdziwym wyzwaniem jest opanowanie większej liczby narzędzi.

Node.js.

Express.

PostgreSQL.

Redis.

Docker.

Kolejki wiadomości.

Projektowanie systemów.

Jednak po wdrożeniu kilku aplikacji staje się jasna inna prawda.

Znajomość większej liczby narzędzi nie czyni automatycznie lepszym inżynierem backendu.

Największy postęp pochodzi z popełniania błędów, zrozumienia, dlaczego się one zdarzyły, oraz upewnienia się, że nie powtórzą się ponownie.

Oto siedem błędów w backendzie, z których warto się nauczyć, wraz z lepszym podejściem.

1. Umieszczanie wszystkiego w kontrolerze

To jest zazwyczaj jeden z pierwszych błędów, na które natrafiają ludzie.

Koniec punktu końcowego może wyglądać w ten sposób:

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);
});

Funkcjonuje.

Ale spójrzcie, ile obowiązków teraz ma ta jedna funkcja:

  • Walidacja
  • Zapytania do bazy danych
  • Zasady biznesowe
  • Sprawdzanie stanu zapasów
  • Budowa zamówień
  • E-maile
  • Odpowiedzi HTTP

Gdy projekt stale rośnie, funkcje obejmujące tak wiele zadań przekształcają się w trudne do zarządzania bloki logiki.

Rozwiązaniem jest rozdzielenie obowiązków.

Request
   ↓
Controller
   ↓
Service
   ↓
Repository
   ↓
Databas

Zadaniem kontrolera jest obsługa protokołu HTTP.

Szara warstwa odpowiada za logikę biznesową.

Warstwa repositoria zarządza dostępem do danych.

To nie oznacza, że mała aplikacja musi mieć sześć warstw abstrakcji.

Oznacza to, że każda część systemu powinna mieć jasno określone zadanie.

2. Zaufanie do interfejsu użytkownika

Ta pomyłka może prowadzić do błędów, a co gorsza – luk bezpieczeństwa.

Załóżmy, że frontend wysyła ten payload:

{
  "price": 10,
  "quantity": 2
}

Kuszące jest użycie ceny przesłanej przez klienta do obliczenia łącznej kwoty zamówienia.

Nie rób tego.

Nic nie powstrzymuje złośliwego klienta przed wysłaniem:

{
  "price": 1,
  "quantity": 100
}

Frontend znajduje się pod kontrolą użytkownika, a nie twoją.

Twój backend musi weryfikować i egzekwować reguły, które są naprawdę istotne.

Naprzykład:

const product = await productRepository.findById(
  productId
);
const total = product.price * quantity;

Źródłem prawdy dotyczącej cen powinien być backend, a nie klient.

Ta sama ostrożność musi zostać zastosowana w kilku innych obszarach, na które klient może próbować wpłynąć:

  • Rolie użytkowników
  • Prawa dostępu
  • Zniżki
  • Zasoby magazynowe
  • Kwoty płatności
  • Stan konta
  • Własność zasobów

Rozważ frontend jako narzędzie do kształtowania sposobu, w jaki użytkownicy korzystają z twojego produktu.

To nie jest granica bezpieczeństwa.

3. Zły obsługa błędów

Na początku obsługa błędów często wyglądała w ten sposób:

try {
  // something
} catch (error) {
  console.log(error);
  return res.status(500).json({
    message: "Something went wrong"
  });
}

Nie ma nic złego w użyciu ogólnego rozwiązania awaryjnego.

Problem polega na poleganiu na nim we wszystkich sytuacjach.

Brak rekordu użytkownika nie musi koniecznie oznaczać błędu poziomu 500.

Błędnie sformatowana prośba nie musi koniecznie oznaczać błędu poziomu 500.

Duplikat adresu e-mail nie musi koniecznie oznaczać błędu poziomu 500.

Część serwerowa musi odróżniać różne typy niepowodzeń.

Naprzимер:

400 → Invalid request
401 → Authentication required
403 → Not allowed
404 → Resource not found
409 → Conflict
422 → Validation failure
500 → Unexpected server error

Kod statusu, który wybierzesz, zależy od konwencji twojej API, ale spójność jest ważniejsza niż dokładny schemat.

Strukturalne odpowiedzi na błędy również pomagają.

Naprzимер:

{
  "success": false,
  "message": "User already exists",
  "code": "USER_ALREADY_EXISTS"
}

Dzięki takiemu formacie frontend nie musi zgadywać, co poszło nie tak.

4. Hardcoding konfiguracji

Błąd ten wydaje się nieszkodliwy, dopóki nie spróbujesz go wdrożyć.

Cokolwiek takiego:

const databaseUrl =
  "postgresql://user:password@localhost:5432/app";

Lub:

const jwtSecret = "my-secret";

Unikaj całkowicie tego wzorca.

Każde środowisko, w którym działasz, wymaga własnych ustawień.

Możesz mieć:

Development
    ↓
localhost
Staging
    ↓
staging databaseProduction
    ↓
production database

Zamiast tego polegaj na konfiguracji opartej na środowisku:

DATABASE_URL=
REDIS_URL=
JWT_SECRET=
PAYMENT_API_KEY=
EMAIL_API_KEY=

I nigdy nie przechowuj tajemnic w systemie kontroli wersji.

Plik .env nadaje się do rozwoju lokalnego, ale środowiska produkcyjne wymagają odpowiednich narzędzi do zarządzania tajemnicami i konfiguracją.

Główna zasada jest następująca:

Twój kod nie powinien być ściśle powiązany z wartościami konfiguracji specyficznymi dla danego środowiska.

5. Skalowanie przed faktyczną potrzebą

To pułapka, w którą wpadają wielu programistów w pewnym momencie, i łatwo jest ją wtedy usprawiedliwić.

Zespół rozpoczyna nowy projekt i od razu przechodzi do:

"Co się stanie, jeśli jutro pojawi się 10 milionów osób?"

Dlatego dodają:

Microservices
Kafka
Redis
Kubernetes
Multiple databases
API Gateway
Event-driven architecture

Na papierze system może obsłużyć ogromną skalę.

Tylko że jest tam zaledwie pięciu rzeczywistych użytkowników.

To nie jest solidna architektura. To złożoność, której nikt jeszcze nie potrzebuje.

Dla większości projektów sensowniej jest zacząć od prostoty:

Client
  ↓
Node.js Application
  ↓
PostgreSQL

I wprowadzać nowe elementy tylko wtedy, gdy istnieje konkretny powód:

Need caching?
→ Redis
Need background jobs?
→ Queue + WorkerNeed more API capacity?
→ Multiple instances + Load BalancerDatabase becoming a bottleneck?
→ Optimize queries / indexes / architecture

Pozwól architekturze rozwijać się wraz z rzeczywistymi, udowodnionymi potrzebami.

Nie sięgaj po system rozproszony tylko dlatego, że jakieś wideo twierdzi, iż właśnie tak postępują „prawdziwi” doświadczeni inżynierowie.

6. Blokowanie żądania przy wolnej pracy

To może w cichu zrujnować doświadczenie korzystania z API.

Wyobraźmy sobie taką konfigurację:

app.post("/order", async (req, res) => {
  const order = await createOrder();  await sendEmail();  await generateInvoice();  await notifyWarehouse();  await updateAnalytics();  return res.json(order);
});

Tutaj użytkownik czeka, aż wszystkie pięć kroków zostanie ukończonych kolejno.

Jeśli choć jeden zewnętrzny wywołanie zajmie pięć dodatkowych sekund, cała odpowiedź będzie o pięć sekund wolniejsza.

Lepszym podejściem jest wykonywanie w ramach żądania tylko tych operacji, które naprawdę muszą zostać przeprowadzone natychmiast.

Wszystko inne może zostać umieszczone w kolejce do przetwarzania w tle.

Client
  ↓
API
  ↓
Create Order
  ↓
Queue Jobs
  ↓
Response

Następnie:

Queue
  ↓
Worker
  ├── Send Email
  ├── Generate Invoice
  ├── Notification
  └── Analytics

To jest dokładnie taki scenariusz, w którym rozwiązania takie jak BullMQ w połączeniu z Redis okazują się przydatne.

Jest tu jednak druga, łatwa do przeoczenia lekcja:

Zadania w tle muszą być idempotentne i umieć sprawnie radzić sobie z awariami.

Jeśli zadanie zostanie przypadkowo wykonyane dwa razy, ryzyka obejmują:

  • Wystawienie rachunku klientowi więcej niż raz
  • Wysłanie duplikatowych powiadomień
  • Zapisanie duplikatowych rekordów w bazie danych

Samo przeniesienie pracy do kolejki nie rozwiązuje tego problemu.

Samo zadanie musi zostać zaprojektowane z tym uwzględnionym.

7. Praca bez widoku w środowisku produkcyjnym

To błąd zazwyczaj pozostaje niewidoczny aż do chwili, gdy coś faktycznie się zepsuje.

Załóżmy, że API w środowisku produkcyjnym nagle zaczyna generować błędy.

Na pierwszy rzut oka serwer wygląda normalnie.

Kod również wydaje się w porządku.

Ale oto co jest brakujące:

No useful logs
No request IDs
No metrics
No error tracking
No database monitoring

W tym momencie jedyną opcją pozostaje zgadywanie.

Debugowanie zamienia się w serię wzruszeń ramionami:

„Może Redis przestał działać?” „Może baza danych zwolniła do minimum?” „Może dostawca płatności ma problemy?”

Jako inżynier znajdujesz się w nieprzyjemnej sytuacji.

Przynajmniej przydatne logi są niezbędne.

Naprzykład:

{
  "level": "error",
  "requestId": "req_123",
  "route": "/orders",
  "userId": "user_456",
  "message": "Payment provider timeout"
}

Dzięki takiemu wyjściu można dokładnie ustalić, co poszło nie tak i gdzie.

Gdy systemy rozwijają się, możliwości obserwacji zazwyczaj rozszerzają się o:

  • Logi aplikacji
  • Śledzenie błędów
  • Użytek CPU i pamięci
  • Metryki na poziomie bazy danych
  • Czas odpowiedzi API
  • Rozmiar kolejki z zaległymi zadaniami
  • Błędy pochodzące z zewnętrznych API
  • Końcówki do sprawdzania stanu zdrowia systemu

System nie może pozostać sprawny, jeśli nie ma możliwości obserwacji jego zachowania.

Ważniejsza lekcja

Gdy spojrzymy szerzej, prawie wszystkie te problemy wynikają z tego samego podstawowego nawyku.

Pytanie, które kieruje wieloma decyzjami, zazwyczaj brzmi:

">Czy ta funkcja działa?"

podczas gdy powinno ono brzmieć:

">Czy tę funkcję łatwo jest zmodyfikować, naprawić błędy i uruchomić w środowisku produkcyjnym?"

Taka zmiana sposobu formułowania pytań wpływa na niemal wszystko w procesie tworzenia oprogramowania.

Czego sprawdzić przed uznaniem API za gotowe

Zanim oznaczymy funkcję backendu jako ukończoną, warto przeprowadzić następujące sprawdzenia.

Kod

  • Czy każda warstwa ma jasną, jedyną odpowiedzialność?
  • Czy logikę biznesową można przetestować w izolacji?
  • Czy kontrolery są utrzymywane w rozsądnie zwięzłej formie?

Bezpieczeństwo

  • Czy cały przychodzący dane wejściowe są walidowane?
  • Czy sprawdzanie uprawnień jest realizowane po stronie serwera?
  • Czy tajne dane są trzymane poza zasięgiem?
  • Czy autoryzacja i uwierzytelnianie są prawidłowo wdrożone?
  • Baza danych

    • Czy zapytania są wydajne?
    • Czy istnieją odpowiednie indeksy?
    • Czy transakcje są używane tam, gdzie jest to konieczne?
    • Czy występuje ukryty problem z zapytaniami typu N+1?

    Wydajność

    • Czy eliminowano możliwe do uniknięcia sekwencyjne wywołania?
    • Czy obciążone operacje są przenoszone do procesu w tle?
    • Czy buforowanie mogłoby poprawić sytuację?

    Niezawodność

    • Jaka jest alternatywa, jeśli zewnętrzna API zawiedzie?
    • Czy próby ponownego wykonania są prawidłowo skonfigurowane?
    • Czy zadania w tle można bezpiecznie uruchamiać więcej niż raz?
    • Co się stanie, jeśli Redis lub baza danych staną się niedostępne?

    Operacje

    • Czy można dowiedzieć się, co się stało, gdy coś zawiedzie?
    • Czy logi są rzeczywiście przydatne?
    • Czy istnieją sprawdzania stanu?
    • Czy można zmierzyć wydajność API?

    Cel nie jest posiadanie doskonałego systemu.

    Jednak kluczowe jest wiedza o tym, co dzieje się w momencie wystąpienia problemu.

    7 błędów na pierwszy rzut oka

    Wolne operacje blokujące żądania
    Błąd Lepsze podejście
    Wszystko umieszczone w kontrolerach Rozdzielenie obowiązków pomiędzy warstwami
    Zaufanie do danych z frontendu
    Jednolite rozwiązywanie błędów Stosowanie spójnej strategii obsługi błędów
    Tajne dane zapisane w kodzie Odpowiednie zarządzanie środowiskiem i konfiguracją
    Zbyt wczesne skalowanie Skalowanie w odpowiedzi na rzeczywiste wąskie gardła
    Dodaj logowanie i monitorowanie

    Ostateczne wnioski

    Panuje powszechne przekonanie, że rozwój jako programista backendu oznacza jedynie gromadzenie coraz większej liczby narzędzi i technologii.

    Bardziej dokładny pogląd mówi, że chodzi tu przede wszystkim o zrozumienie kompromisów.

    Czy ta operacja powinna być synchroniczna czy asynchroniczna?

    Czy te dane warto zapamiętywać w pamięci podręcznej?

    Czy ta zapytanie wymaga indeksu?

    Czy to powinno być oddzielne usługa?

    Jaki jest plan na wypadek awarii Redis?

    Jaki jest plan na wypadek spowolnienia bazy danych?

    Jaki jest plan na wypadek przypadkowego uruchomienia zadania dwa razy?

    Jaki jest plan na wypadek awarii zewnętrznej API, od której zależy działanie?

    Znajomość odpowiedzi na te pytania jest o wiele ważniejsza niż sama umiejętność instalacji kolejnego pakietu.

    Ponieważ systemy produkcyjne nie są oceniane po tym, jak zachowują się, gdy wszystko idzie gładko.

    Prawdziwa inżynieria pokazuje się w momencie, gdy coś idzie nie tak.

    A każdy błąd rozpoznany dziś oznacza mniej pożarów w produkcji do gaszenia później.

    Literatura pokrewna

  • Wybór między Promise.all, Promise.race a sekwencyjnymi oczekiwaniami — Dowiedz się, kiedy Promise.all() przyspiesza API Node.js, dlaczego szybko zawodzi przy każdym odrzuceniu oraz jak wybrać odpowiedni wzorzec asynchroniczny.
  • Dlaczego wzory regex z backtrackingiem mogą cicho zablokować twój serwer — Przeczytaj, jak chciwe dopasowywanie i katastrofalne backtracking mogą przekształcić pozornie poprawny wzorzec regex w problem blokujący CPU w produkcji oraz jak rozpoznać takie ryzyko.
  • REST vs GraphQL: Prawdziwe kompromisy stojące za każdą architekturą — Wyjaśnia konkretne problemy, które rozwiązują REST i GraphQL, ich wewnętrzną mechanikę oraz ukryte kompromisy, które należy wziąć pod uwagę przed wyborem jednego z nich do swojej API.
  • Poza P95: Pomiar opóźnienia, którego faktycznie doświadczają Twoi użytkownicy — Dlaczego dobry wskaźnik P95 może współistnieć z wolnym produktem, jak czas oczekiwania w kolejce i rozprzestrzenianie zapytań ukrywają się przed panelami kontrolnymi oraz jak mierzenie czasu na każdy krok kończy spory o przyczyny opóźnień.
  • Sześć wzorów integracji dla niezawodnego łączenia usług Node.js — Poznaj podstawowe wzory leżące u podstaw solidnych integracji w Node.js: model żądanie-odpowiedź, polling, webhooks, klucze API, autoryzacja JWT i OAuth, ponawianie prób z opóźnieniem oraz mapowanie danych.