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.
openapi: 3.1.2
info:
title: Billing API
version: 1.8.0
paths:
/v1/invoices/{id}:
get:
responses:
'200':
description: OKDecyzję o version bump opieraj na regule:
- Jeśli klient musi zmienić kod, traktuj to jako breaking.
- Jeśli breaking dotyczy wielu klientów, dodaj nową wersję i okres migracji.
- 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
POST /v1/orders
Idempotency-Key: 4f3bb3ff-3328-4bc2-a70a-59efec9db195Serwer 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):
{
"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
typez 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
ETagdla reprezentacji, - klient wysyła
If-Matchprzy mutacji, - gdy wersja się rozjechała, serwer zwraca
412 Precondition Failedalbo wymaga preconditions przez428 Precondition Required.
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/prevw response lubLinkheader.
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
429i503, Retry-Aftergdy 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
- Operacja z efektem ubocznym i bez idempotencji -> nie retry automatycznie.
- Operacja idempotentna -> retry z limitem, backoffem i jitterem.
- 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
Deprecationheader (RFC 9745),Sunsetheader (RFC 8594),Link rel="deprecation"do planu migracji,- dashboard adopcji nowej wersji per client/app.
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
- Ustal formalną politykę "breaking vs non-breaking" i włącz OAS diff gate w CI.
- Wprowadź idempotency key dla wszystkich mutacji o skutkach biznesowych.
- Ustandaryzuj błędy do RFC 9457 i przypisz polityki retry po
type. - Dodaj
ETag/If-MatchdlaPUT/PATCHkrytycznych zasobów. - Zamień offset na cursor dla list o wysokiej mutowalności.
- Uzgodnij globalny retry budget per klient i per endpoint.
- Dokumentuj
429/503orazRetry-Afterw kontrakcie. - Dodaj
DeprecationiSunsetdo wersji przeznaczonych do wygaszenia. - Zbuduj dashboard adopcji wersji per token/client id.
- 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.
