Architektura

Kontrakty API odporne na zmiany

Kontrakt jest mocny, gdy czyni zmianę jawną, testowalną i nudną dla konsumentów — pełna powierzchnia kontraktu, prawdziwa polityka kompatybilności, kształty błędów i mała macierz testów, która tego wszystkiego pilnuje.

12 lut 2026 10 min czytania Rinkachi
  • API
  • Kontrakty
  • Testowanie
  • Architektura
Udostępnij LinkedIn X

TL;DR: Kontrakt API to wszystko, co konsument może zaobserwować i na czym może polegać — nie tylko schemat. Nazwij tę powierzchnię jawnie, opublikuj politykę kompatybilności, na której konsumenci mogą planować, ustandaryzuj kształty błędów, wersjonuj z konieczności, nie z nawyku, i pilnuj tego wszystkiego małą, nudną macierzą testów uruchamianą przy każdej zmianie.

Nazwij powierzchnię kontraktu

Każda awaria integracji, jaką badałem, sprowadza się do tego samego nieporozumienia: dostawca myślał, że kontraktem jest plik OpenAPI, a konsument myślał, że kontraktem jest wszystko, co API kiedykolwiek obserwowalnie zrobiło. Konsument ma rację — zgodnie z prawem Hyruma każde obserwowalne zachowanie w końcu zostanie od czegoś uzależnione, niezależnie od tego, czy je udokumentowałeś.

Pierwszym aktem projektowania kontraktu jest więc nazwanie pełnej powierzchni, która jest dużo szersza niż schemat:

  • Kody statusu i ich znaczenie — łącznie z tym, które nadają się do ponowienia, a które są ostateczne.
  • Zachowanie paginacji — stabilność kolejności, okno ważności kursora, co dzieje się po ostatniej stronie.
  • Zachowanie autoryzacji — semantyka wygasania tokenu, co faktycznie odróżnia 401 od 403.
  • Idempotencja — które operacje bezpiecznie ponawiać i jakim mechanizmem klucza.
  • Kształty błędów — struktura porażki, którą konsumenci parsują, czy tego chcesz, czy nie.
  • Rate limity — sufity, nagłówki je zapowiadające, zachowanie przy przekroczeniu.
  • Znaczniki czasu, nulle, kodowania — konwencje stref czasowych, semantyka braku wartości kontra null, precyzja liczb.
  • Zasady cyklu życia — jak zmienia się sam kontrakt: powiadomienie o deprecjacji, okna wygaszania, wsparcie migracji.

Wszystko z tej listy, co zostanie niesprecyzowane, nie pozostaje elastyczne — staje się przypadkowym kontraktem, zamrożonym przez pierwszego konsumenta, który uzależni się od dzisiejszego, incydentalnego zachowania.

Kompatybilność to obietnica produktowa

Konsumenta nie obchodzi, że pole było wygodnie przemianować. Obchodzi go, czy integracja nadal działa, gdy jest na urlopie. Kompatybilność jest więc obietnicą produktową i zasługuje na jawność takiej obietnicy — pisemną politykę mówiącą, co może się zmienić bez zapowiedzi, co wyzwala wersję i jak długo przeżywa stare zachowanie.

Zasada addytywności pokrywa większość przypadków: dodawanie opcjonalnych pól, nowych endpointów i nowych wartości enum jest kompatybilne; usuwanie albo przemianowanie czegokolwiek, zmiana typów, zaostrzanie walidacji albo zmiana semantyki jest łamiące. Dwa z nich zasługują na ostrzeżenie z doświadczenia. Nowe wartości enum łamią konsumentów przełączających się wyczerpująco na starym zbiorze — więc deklaruj enumy jako otwarte w kontrakcie i wymagaj gałęzi „nieznane". A zaostrzona walidacja to najbardziej ukradkowa łamiąca zmiana na liście: żądania działające wczoraj, a zawodzące dziś, są łamiącą zmianą niezależnie od tego, co mówi plik schematu.

Polityka domyślna

„Staramy się nie łamać rzeczy." Konsumenci odkrywają politykę empirycznie, incydent po incydencie, i wliczają ryzyko w każdą decyzję integracyjną.

Polityka jawna

„Zmiany addytywne wychodzą w każdej chwili. Łamiące dostają nową wersję, 6 miesięcy równoległego działania i nagłówki deprecjacji od pierwszego dnia." Konsumenci planują; zaufanie procentuje.

Kształty błędów to część kontraktu

Odpowiedzi błędów parsują maszyny i czytają zmęczeni ludzie o drugiej w nocy — obie publiczności zasługują na spójność. Stabilna koperta błędu — jeden kształt na wszystkich endpointach — to jedna z najtańszych inwestycji jakościowych, jakie może zrobić zespół API, a RFC 9457 daje standard, żebyś nie musiał wymyślać własnego:

{
  "error": "validation_failed",
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
  "fields": {
    "email": ["must be a valid email address"]
  }
}

Trzy właściwości znaczą więcej niż dokładny kształt. Stabilne, czytelne maszynowo kody — konsumenci rozgałęziają się na validation_failed, nigdy na tekście komunikatu, więc tekst komunikatu zostaje wolny do poprawiania. Identyfikator trace'u w każdym błędzie, który zamienia tickety wsparcia „nie działa" w pięciominutowe wyszukiwania. I komunikaty bezpieczne do wyświetlenia: żadnych stack trace'ów, wewnętrznych hostname'ów, SQL-a — kształt błędu to też powierzchnia bezpieczeństwa.

Wersjonowanie i cykl życia

Debaty o wersjonowaniu pochłaniają dużo więcej energii, niż na to zasługują; mechanizm (ścieżka, nagłówek, media type) znaczy dużo mniej niż polityka. Moje domyślne ustawienia: wersjonuj z konieczności, nie z kalendarza — nowa wersja to przyznanie, że musisz kogoś złamać, a każda żyjąca wersja mnoży utrzymanie i wsparcie na zawsze. Preferuj ewolucję kompatybilną w obrębie wersji tak długo, jak pozwala uczciwość.

Gdy złamanie jest naprawdę konieczne, ciężką pracę wykonuje cykl życia: ogłoś nagłówkami Deprecation i Sunset plus changelogiem i bezpośrednim dotarciem do znanych konsumentów, uruchom starą i nową równolegle przez zadeklarowane okno i obserwuj ruch starej wersji — deprecjacja bez monitorowania adopcji to odliczanie do niespodziewanej awarii, tylko z dodatkowymi krokami.

Utrzymaj małą macierz testów

Kontrakt jest realny tylko wtedy, gdy coś zawodzi, kiedy zostaje naruszony. Mała macierz kontroli po stronie dostawcy, uruchamiana w CI przy każdej zmianie, pokrywa obszary, w których regresje naprawdę się zdarzają:

ObszarCheck kontraktu
Kształt błęduSnapshot przykładowych odpowiedzi błędów per klasa endpointu
PaginacjaPusty wynik, pierwsza, środkowa, ostatnia strona; ponowne użycie kursora po wygaśnięciu
IdempotencjaPonowione tworzenie z tym samym kluczem daje dokładnie jeden efekt
Diff schematuWygenerowany OpenAPI porównany z zatwierdzonym — łamiący diff wywala build
Tolerancja enumUdokumentowane enumy zadeklarowane jako otwarte; przykładowa wartość „unknown" przechodzi round-trip
Semantyka autoryzacjiWygasły token daje 401 z udokumentowanym kodem błędu, nie 500

Check diffu schematu zasługuje na osobną wzmiankę: automatyczne wykrywanie łamiących zmian (istnieją gotowe narzędzia dla OpenAPI) zamienia kompatybilność z oceny podejmowanej podczas review w porażkę builda — dokładnie tam, gdzie chcesz, żeby ten spór się odbywał.

Kontrole napędzane przez konsumentów

Testy po stronie dostawcy weryfikują to, co obiecałeś; testy kontraktowe napędzane przez konsumentów weryfikują to, czego konsumenci faktycznie używają — co rutynowo jest zbiorem innym, mniejszym i czasem zaskakującym. W przepływach w stylu Pact każdy konsument publikuje interakcje, od których zależy, a dostawca odtwarza je w CI: zmiana przechodząca własny zestaw testów, ale łamiąca realnego konsumenta, zawodzi przed merge'em zamiast na produkcji.

Uczciwa ocena kosztu: infrastruktura contract-testing zarabia na siebie, gdy konsumenci są zewnętrzni albo liczni albo gdy dostawca i konsument wdrażają się niezależnie. Dla dwóch wewnętrznych usług należących do jednego zespołu tańszy jest współdzielony test integracyjny, łapiący tę samą klasę problemów. Adoptuj maszynerię, gdy koszt koordynacji, który zastępuje, jest realny.

Uwagi z produkcji

  • Wypatruj kontraktów de facto. Okresowo porównuj realny ruch z udokumentowanym kontraktem. Nieudokumentowane zachowanie z dużym użyciem to kontrakt, który już masz — albo je udokumentuj, albo zaplanuj jego usunięcie jako właściwą łamiącą zmianę.
  • Changelog jako artefakt pierwszej klasy. Konsumenci integrują się z changelogiem bardziej niż z dokumentacją referencyjną. Datuj każdą zmianę, oznaczaj addytywne kontra łamiące, linkuj notatki migracyjne.
  • Odpowiedzi rate limitu to też kontrakt. Kształt 429, nagłówek Retry-After i nagłówki pozostałego limitu powinny być udokumentowane i testowane jak każdy endpoint — konsumenci budują na nich logikę backoffu.
  • Parytet sandboxa. Środowisko testowe, którego zachowanie odbiega od produkcji, uczy konsumentów złych lekcji, które wypływają jako incydenty produkcyjne po ich stronie i obciążenie wsparcia po Twojej.

Podsumowanie

  • Kontrakt to wszystko, co obserwowalne — nazwij pełną powierzchnię albo zobacz, jak zamarza przypadkiem.
  • Publikuj politykę kompatybilności; traktuj zaostrzoną walidację i zamknięte enumy jako ukradkowe złamania, którymi są.
  • Jedna koperta błędu wszędzie: stabilne kody, trace ID, komunikaty bezpieczne do wyświetlenia.
  • Wersjonuj z konieczności; przy złamaniu prowadź okna równoległe i monitoruj adopcję starej wersji aż do zera.
  • Pilnuj kontraktu małą macierzą CI — diff schematu, kształty błędów, paginacja, idempotencja.
  • Dodaj testy napędzane przez konsumentów, gdy konsumenci są zewnętrzni, liczni albo wdrażani niezależnie.

Budujesz systemy rozproszone?

Zobacz, jak pomagam w system design, niezawodności i decyzjach architektonicznych.

Sprawdź system design