EngineeringAPIArchitektura

Błędy w projektowaniu API, które wychodzą na jaw dopiero po roku w produkcji

7 błędów projektowania API, które nie bolą na starcie, ale po roku blokują zmiany: kompatybilność, idempotencja, model błędów, paginacja i deprecacja.

Błędy w projektowaniu API, które wychodzą na jaw dopiero po roku w produkcji

Krótka odpowiedź

Najdroższe błędy API to zwykle nie błędy składni endpointów, tylko brak kontraktów na zmianę, retry i wygaszanie.

Przez pierwsze miesiące system działa, bo ruch jest mały, klienci są nieliczni, a zmiany są ręcznie koordynowane.

Po roku skala rośnie, integracji jest więcej i te same decyzje zaczynają generować regresje, duplikaty danych i kosztowne migracje.

Dlaczego te problemy wychodzą dopiero po roku

Na starcie API jest testowane głównie pod kątem "czy działa happy path".

Po 12 miesiącach pojawiają się warunki, których zwykle nie ma w MVP:

  • wiele wersji klientów mobilnych jednocześnie,
  • retry z różnych SDK i integratorów,
  • większa konkurencja zapisu na tych samych zasobach,
  • ewolucja modelu danych bez pełnej kontroli nad wszystkimi konsumentami.

To moment, w którym "działa" przestaje znaczyć "jest bezpieczne w utrzymaniu".

1) Brak polityki kompatybilności wstecznej

Najczęstszy błąd: traktowanie każdej zmiany jako wewnętrznej refaktoryzacji, mimo że dla klienta jest to zmiana kontraktu.

Kiedy zmiana jest breaking

W praktyce breaking change to każda zmiana, która może zepsuć istniejącego klienta bez jego deployu, np.:

  • usunięcie pola z response,
  • zmiana typu pola (int -> string),
  • zaostrzenie walidacji requestu,
  • zmiana semantyki kodu statusu.

Jeśli nie masz formalnej polityki kompatybilności, każdy release staje się loterią.

Jak to naprawić

Utrzymuj kontrakt w OpenAPI i waliduj diff w CI.

yaml
openapi: 3.1.2
info:
  title: Billing API
  version: 1.8.0
paths:
  /v1/invoices/{id}:
    get:
      responses:
        '200':
          description: OK

Decyzję o version bump opieraj na regule:

  1. Jeśli klient musi zmienić kod, traktuj to jako breaking.
  2. Jeśli breaking dotyczy wielu klientów, dodaj nową wersję i okres migracji.
  3. Jeśli zmiana jest additive, utrzymuj wersję i monitoruj adopcję.

Trade-off: więcej wersji API zwiększa koszt utrzymania, ale brak wersjonowania zwiększa koszt incydentów i rollbacków.

2) Retry bez idempotencji mutacji

Po roku ruch rośnie i retry pojawia się wszędzie: load balancer, SDK, job worker, integrator.

Jeśli POST /orders nie ma idempotency key, chwilowy timeout może stworzyć dwa zamówienia.

Wzorzec produkcyjny

http
POST /v1/orders
Idempotency-Key: 4f3bb3ff-3328-4bc2-a70a-59efec9db195

Serwer powinien zwracać ten sam wynik dla tego samego klucza i tego samego payloadu w oknie retencji.

Praktyka z dużych API płatniczych (np. Stripe) pokazuje, że to najprostszy sposób ograniczenia duplikatów przy retry.

Kryterium decyzji

  • endpoint ma efekty uboczne -> wymagaj idempotency key,
  • endpoint read-only -> polegaj na idempotencji metody HTTP,
  • endpoint high-value (płatności, provisioning) -> wydłuż retencję kluczy i audytuj kolizje.

Trade-off: przechowywanie kluczy kosztuje storage i logikę porównywania payloadu, ale brak idempotencji kosztuje ręczne naprawy danych.

3) Błędy bez stabilnego modelu machine-readable

Jeśli API zwraca tylko {"message":"coś poszło nie tak"}, klient nie wie, czy ma retry, czy poprawić payload, czy eskalować do operatora.

Po roku kończy się to if-ologią po stringach i nieprzewidywalnym zachowaniem integracji.

Lepszy kontrakt

Ustandaryzuj błędy przez application/problem+json (RFC 9457):

json
{
  "type": "https://api.example.com/problems/insufficient-quota",
  "title": "Quota exceeded",
  "status": 429,
  "detail": "Daily write quota exceeded",
  "instance": "/v1/orders/req-98f1"
}

Klient mapuje type na politykę reakcji. To skaluje się lepiej niż parsowanie tekstu detail.

Kryterium decyzji

  • błąd powtarzalny i automatycznie obsługiwalny -> stały type,
  • błąd domenowy -> osobny namespace type z dokumentacją,
  • błąd tymczasowy -> status 5xx/429 + jawna polityka retry.

4) Brak kontraktu współbieżności przy update

Przy małym ruchu dwa równoległe PATCH na tym samym zasobie to rzadkość.

Po roku, przy async jobach i wielu konsumentach, klasyczny lost update staje się regularnym źródłem regresji.

Kontrakt, który działa

  • serwer zwraca ETag dla reprezentacji,
  • klient wysyła If-Match przy mutacji,
  • gdy wersja się rozjechała, serwer zwraca 412 Precondition Failed albo wymaga preconditions przez 428 Precondition Required.
http
PATCH /v1/subscriptions/sub_123
If-Match: "v17"

To przenosi konflikt do jawnego kontraktu API zamiast cichego nadpisania danych.

Trade-off: dodatkowy krok po stronie klienta, ale dużo mniejszy koszt naprawy niespójnych danych.

5) Offset pagination na kolekcjach o wysokiej mutowalności

Offset działa w demo, ale po roku przy dużym ruchu daje duplikaty i "znikające" rekordy między stronami.

Wystarczy, że między page=1 i page=2 ktoś dopisze nowy rekord.

Stabilniejszy wzorzec

  • cursor pagination,
  • deterministyczny sort (created_at DESC, id DESC),
  • linki next/prev w response lub Link header.
http
Link: </v1/events?cursor=eyJjcmVhdGVkX2F0Ijoi..."; rel="next"

Kryterium decyzji

  • mała, prawie statyczna kolekcja -> offset może zostać,
  • duża lub często mutowana kolekcja -> cursor jest domyślny,
  • integracje ETL -> wymagaj snapshot/cutoff semantics.

Trade-off: cursor jest trudniejszy debugowo, ale stabilniejszy semantycznie i tańszy operacyjnie przy skali.

6) Brak budżetu timeout/retry/backoff

Bez limitu retry klient "ratuje" pojedyncze żądanie kosztem całego systemu.

Pod przeciążeniem to prowadzi do retry storm i wydłuża czas powrotu systemu do normy.

Minimalny kontrakt

  • jawne statusy 429 i 503,
  • Retry-After gdy serwer wie, kiedy klient ma wrócić,
  • rekomendacja exponential backoff + jitter,
  • limit prób per request (retry budget).

AWS Builders Library trafnie pokazuje, że retry są "selfish" i mogą wzmacniać awarię zależności.

Kryterium decyzji

  1. Operacja z efektem ubocznym i bez idempotencji -> nie retry automatycznie.
  2. Operacja idempotentna -> retry z limitem, backoffem i jitterem.
  3. Krytyczny downstream przeciążony -> preferuj fail-fast nad kaskadowe timeouty.

7) Deprecation bez dat i telemetrii migracji

Najbardziej bolesny antywzorzec: "v1 jest deprecated" w changelogu, bez sygnału w samym API.

Po roku utrzymujesz martwą wersję, bo nie wiesz, kto jeszcze jej używa.

Kontrakt wygaszania

  • Deprecation header (RFC 9745),
  • Sunset header (RFC 8594),
  • Link rel="deprecation" do planu migracji,
  • dashboard adopcji nowej wersji per client/app.
http
Deprecation: @1743465600
Sunset: Tue, 30 Sep 2026 23:59:59 GMT
Link: <https://docs.example.com/migrate-v2>; rel="deprecation"

Kryterium decyzji

  • brak migracji > 80% ruchu do nowej wersji -> wydłuż okno,
  • niski udział starych klientów + pełna komunikacja -> domykaj wersję,
  • brak telemetrycznych danych adopcji -> nie wyłączaj endpointu.

Checklista naprawcza na 90 dni

  1. Ustal formalną politykę "breaking vs non-breaking" i włącz OAS diff gate w CI.
  2. Wprowadź idempotency key dla wszystkich mutacji o skutkach biznesowych.
  3. Ustandaryzuj błędy do RFC 9457 i przypisz polityki retry po type.
  4. Dodaj ETag/If-Match dla PUT/PATCH krytycznych zasobów.
  5. Zamień offset na cursor dla list o wysokiej mutowalności.
  6. Uzgodnij globalny retry budget per klient i per endpoint.
  7. Dokumentuj 429/503 oraz Retry-After w kontrakcie.
  8. Dodaj Deprecation i Sunset do wersji przeznaczonych do wygaszenia.
  9. Zbuduj dashboard adopcji wersji per token/client id.
  10. Zdefiniuj runbook: kiedy wydłużasz sunset, kiedy zamykasz endpoint.

Finalny werdykt

Błędy API, które wychodzą po roku, mają wspólny mianownik: brak jawnych reguł ewolucji kontraktu.

Dobre API nie polega na tym, że endpoint "odpowiada 200", tylko na tym, że da się je bezpiecznie zmieniać przy rosnącej skali i liczbie klientów.

Jeżeli dzisiaj wdrożysz kompatybilność, idempotencję, model błędów, preconditions i kontrakt deprecacji, za rok kupisz sobie znacznie mniej nocnych rollbacków.