rewrites nevznikají jen tak
Většina přepisů API se prodává jako technická nevyhnutelnost: starý framework, špatná databáze, nešťastné názvy, pár endpointů z roku 2021, za které se dneska všichni stydí. Tenhle výklad neberu. Drahý rewrite většinou přijde až po dlouhém období neřízených změn, kdy si každý tým shipuje payload tak, jak se mu to zrovna hodí, nikdo netuší, který klient závisí na kterých fieldách, a breaking changes tečou do produkce, protože jediný kontrakt je to, co frontend minulý týden náhodou parsoval.
Viděl jsem tenhle vzorec v SaaS týmech o dvaceti lidech i o stovce, princip je pořád stejný. Jeden squad přejmenuje customer_id na accountId, jiný začne vracet null u fieldu, který se dřív prostě neposílal, mobile si na půl roku připíchne starý response shape, protože App Store review něco trvá, a vedení pak dojde k závěru, že API je „messy“ a chce kompletní náhradu. Ve skutečnosti mají governance problém. Ten šel vyřešit explicitními schématy, compatibility checky v CI a deprecation politikou, za kterou někdo opravdu odpovídá.
Špatná zpráva je, že vás před tím nezachrání žádný framework. Klidně to celé postavíte v Next.js route handlers, Django Ninja, FastAPI, Spring Boot 3.2 nebo čemkoli jiném, a stejně budete každé sprinty posílat do produkce náhodné breakage, pokud kontrakt existuje jen v kolektivní paměti týmu. Dobrá zpráva je, že oprava je nudná, levná a funguje i v malém týmu. Nejdřív napište API schema, uložte ho do repo, validujte ho v CI, přinuťte consumery deklarovat očekávání a každý breaking change protlačte přes vědomé rozhodnutí o verzování.
Zní to těžkopádněji než slide deck pro rewrite. Není. Ve Steezr jsme to takhle dělali na interních ERP systémech i zákaznických portálech, kde byla důležitější rychlost než procesní divadlo. Týmy, které držely kontrakt pevně, se po třetím měsíci hýbaly rychleji, protože přestaly řešit payload shape v pull requestech a přestaly zjišťovat breakage ze screenshotů ve Slacku.
openapi 3.1 jako source of truth
OpenAPI 3.1 je konečně dost dobré na to, aby fungovalo jako skutečný kontrakt, hlavně proto, že se srovnalo s JSON Schema 2020-12 místo vymýšlení vlastního skoro-schématu. To je důležitější, než si lidi připouštějí. Jakmile máte typy, pravidla pro nullability, enumy, formáty a omezení objektů zapsané ve specifikaci, které rozumí standardní nástroje, můžete ji lintovat, diffovat, generovat z ní mocky, validovat requesty a přestat si namlouvat, že TypeScript typy jsou veřejný API kontrakt.
Minimal schema-first setup nepotřebuje žádný platform tým. Dejte openapi.yaml do service repo, reviewujte ho jako kód a shoďte CI, pokud poruší kompatibilitu. Na začátek stačí tohle:
1openapi: 3.1.02info:3 title: Customer API4 version: 1.4.05servers:6 - url: https://api.example.com7paths:8 /customers/{id}:9 get:10 operationId: getCustomer11 parameters:12 - name: id13 in: path14 required: true15 schema:16 type: string17 format: uuid18 responses:19 '200':20 description: Customer found21 content:22 application/json:23 schema:24 $ref: '#/components/schemas/Customer'25components:26 schemas:27 Customer:28 type: object29 required: [id, email, status]30 additionalProperties: false31 properties:32 id:33 type: string34 format: uuid35 email:36 type: string37 format: email38 status:39 type: string40 enum: [active, suspended]41 full_name:42 type: [string, 'null']
Tady mám pár silných názorů. Nastavte additionalProperties: false, pokud nemáte opravdu dobrý důvod to neudělat, protože tichý drift fieldů je přesně to, z čeho se API stává folklor. Nullable hodnoty zapisujte explicitně přes type: [string, 'null'] v 3.1, ne že to nějak odbydete v dokumentaci. Stabilní operationId dejte všude, protože na nich závisí downstream tooling a jejich nahodilé přejmenovávání je zbytečný churn.
Na linting je jasná volba Spectral. Malé .spectral.yaml zachytí dost bordelu:
1extends: [spectral:oas]2rules:3 operation-operationId: error4 operation-tags: off5 no-$ref-siblings: error6 info-contact: off7 oas3-api-servers: warn
Pak to zapojte do CI přes Redocly CLI nebo přímo Spectral. Pokud je spec rozbitá, build má spadnout dřív, než se vůbec začne debatovat o implementaci.
consumer kontrakty chytí skutečné breakage
OpenAPI vám říká, co provider tvrdí, že API je. Consumer-driven contracts vám říkají, na čem klienti skutečně stojí. Potřebujete oboje. Samotné specifikace bez ověřování na straně consumerů rychle ujedou do optimistické fikce, zvlášť když máte webovou appku, mobile appku, partner integraci a nějaký cronem poháněný backend consumer, který volá stejné endpointy z trochu jiných důvodů.
Pact je tady pořád nejpraktičtější volba. Frontend nebo downstream service si nahraje interakce, které potřebuje, publikuje pact a provider ověří, že ta očekávání pořád platí. Hlavní výhoda není žádné fancy tooling, ale to, že vás to donutí vést reálnou debatu o závislostech. Pokud consumer spoléhá na to, že status existuje vždycky a má vždycky hodnotu active nebo suspended, zjistíte to v CI, ne po pátečním deployi.
Jednoduchý consumer test v JavaScriptu s Pact v12 vypadá takhle:
1import { PactV3, MatchersV3 } from '@pact-foundation/pact';2import axios from 'axios';34const provider = new PactV3({5 consumer: 'billing-portal',6 provider: 'customer-api'7});89describe('GET /customers/:id', () => {10 it('returns a billable customer', async () => {11 provider12 .given('customer 8f4c3d8e-4c2e-4f7d-90c0-9fcb2cfdb133 exists')13 .uponReceiving('a request for a customer')14 .withRequest({15 method: 'GET',16 path: '/customers/8f4c3d8e-4c2e-4f7d-90c0-9fcb2cfdb133'17 })18 .willRespondWith({19 status: 200,20 headers: { 'Content-Type': 'application/json' },21 body: {22 id: MatchersV3.uuid('8f4c3d8e-4c2e-4f7d-90c0-9fcb2cfdb133'),23 email: MatchersV3.email('ops@example.com'),24 status: MatchersV3.regex('active|suspended', 'active')25 }26 });2728 await provider.executeTest(async mockServer => {29 const res = await axios.get(`${mockServer.url}/customers/8f4c3d8e-4c2e-4f7d-90c0-9fcb2cfdb133`);30 expect(res.data.status).toBe('active');31 });32 });33});
Provider pak tyhle pacty ověřuje ve své pipeline. Když někdo odstraní status nebo endpoint začne vracet customerStatus, verification spadne. Hlasitě. To je dobře. Přesně v tu chvíli chcete tření.
Tohle samozřejmě vyžaduje disciplínu. Consumeři by měli publishovat pact při každém buildu z main branche. Provideři by je měli ověřovat proti posledním nasazeným verzím consumerů plus proti main, ne jen proti tomu, co je zrovna pohodlné. Když to odfláknete, Pact bude jen další badge v README. Když se používá správně, zastaví ty běžné breakage, které se časem nasčítají do tlaku na rewrite.
ci musí blokovat drift schématu
Governance funguje jen tehdy, když ji vynucuje stroj. Confluence stránka s textem „prosím, vyhýbejte se breaking changes“ je dekorace. CI musí na každém pull requestu odpovědět na čtyři otázky: je OpenAPI soubor validní, drží se vašich pravidel stylu, je zpětně kompatibilní a pořád změny na straně provideru splňují očekávání známých consumerů.
Na diffování OpenAPI je oasdiff výborný a nepříjemně konkrétní. Porovnejte spec z branche proti main, failněte build při breaking changes a vypište přesně, co je špatně. Takový output si inženýři všimnou:
1error: breaking changes detected2- response body property 'status' was removed from GET /customers/{id} 200 application/json3- request parameter 'id' format changed from 'uuid' to 'string' in GET /customers/{id}
GitHub Actions workflow může zůstat úplně malé:
1name: api-contract2on:3 pull_request:4 paths:5 - 'openapi.yaml'6 - 'src/**'7 - '.github/workflows/api-contract.yaml'8jobs:9 contract:10 runs-on: ubuntu-latest11 steps:12 - uses: actions/checkout@v413 with:14 fetch-depth: 015 - uses: actions/setup-node@v416 with:17 node-version: 2018 - name: Install tools19 run: |20 npm i -g @stoplight/spectral-cli @redocly/cli21 curl -L https://github.com/oasdiff/oasdiff/releases/download/v2.8.1/oasdiff_2.8.1_linux_amd64.tar.gz | tar xz22 sudo mv oasdiff /usr/local/bin/23 - name: Lint spec24 run: spectral lint openapi.yaml25 - name: Validate spec26 run: redocly lint openapi.yaml27 - name: Compare with main28 run: |29 git show origin/main:openapi.yaml > openapi-main.yaml30 oasdiff breaking openapi-main.yaml openapi.yaml31 - name: Verify pact32 run: npm run pact:verify
Pokud vaše service běží na Django 5.0 s DRF nebo Ninja, generujte runtime schema jen jako sekundární kontrolu a pak ho porovnejte s verzí commitnutou v repo. Stejný princip platí pro Next.js 14 backend s route handlers a Zod. Primární musí zůstat commitnutý kontrakt, protože generované specifikace často jen odrážejí implementační nehody. Drobné upravení serializeru nemá potichu předefinovat veřejné API.
Ještě jedna věc: shoďte build i tehdy, když endpointy označené jako deprecated zůstávají deprecated navždy. Týmy milují přidat deprecated: true a pak už nic nikdy neuklidit. Governance bez úklidu se změní v usazeniny.
verzování a deprecation, které lidi dodržují
Verzovací politika musí být tak nudná, aby se nikdo každý sprint neptal na výjimku. U veřejných API preferuju URI-major versioning, tedy /v1/customers, /v2/customers, protože je to jasně vidět v logách, dashboardech, zákazníkům i během incidentu. Versioning v hlavičkách se vždycky prodává jako elegantní řešení, a pak půlka toolingu ten header neposílá. Elegance vám při incidentu nepomůže.
Major verze jsou jen pro breaking changes. Přidání fieldů, nové volitelné query parametry, další hodnoty enumu, pokud s nimi consumeři umí počítat, to všechno zůstává ve stejné major verzi. Odstranění fieldu, změna nullability, zpřísnění validačních pravidel, změna významu status codu, to všechno breaking je. Tak se k tomu chovejte. Pokud o tom váš tým pořád dokola debatuje, napište příklady přímo do politiky a přestaňte ten spor řešit znovu.
Deprecation taky potřebuje termíny, ne pocit. Označte field nebo endpoint jako deprecated v OpenAPI, oznamte náhradu, nastavte removal date aspoň 90 dní dopředu pro interní consumery a obvykle víc pro externí, a sledujte skutečné použití v logách. Pokud nikdo neumí odpovědět na otázku „kdo ještě volá /v1/customers/{id} se starým response shape“, jedete naslepo.
OpenAPI to podporuje přímo:
1paths:2 /v1/customers/{id}:3 get:4 deprecated: true5 summary: Deprecated, use /v2/customers/{id}6 responses:7 '200':8 description: Customer found
U fieldů napište deprecation do popisu ve schématu a do changelogu, pak consumery před odstraněním aktivně upozorněte. Ještě lepší je posílat deprecation headery typu Deprecation: true a Sunset: Wed, 31 Jul 2026 23:59:59 GMT u endpointů, které míří ven. Není to overkill. Je to základní provozní hygiena.
Malé týmy se tomu obvykle brání, protože se bojí procesního zpomalení. To je legitimní obava. Trik je v tom mít politiku tak krátkou, aby se vešla na jednu obrazovku, a tak striktní, aby u ní inženýři nemohli improvizovat. Jakmile si to sedne, velocity jde nahoru, protože méně změn vyžaduje archeologii.
checklist na příští týden
Pokud vám roste API surface a nikdo nechce zakládat governance committee, začněte jednoduchým týdenním pravidlem.
Za prvé, každý endpoint, který používá něco mimo danou service, musí být v openapi.yaml a musí se mergnout ve stejném pull requestu jako změna kódu. Bez výjimek, bez tiketů typu „zdokumentujeme později“. Později nikdy nepřijde.
Za druhé, CI musí pouštět lintování spec, compatibility diff proti main a Pact provider verification. Když jeden z těch checků spadne, pull request zůstává červený. Inženýři se přizpůsobí velmi rychle, když je pravidlo skutečné.
Za třetí, každý breaking change potřebuje v PR templatu tři explicitní položky: dotčení consumeři, migrační cesta a datum odstranění. Pokud to autor neumí vyplnit, změna není připravená. Tohle zachytí překvapivě hodně impulzivních úprav API.
Za čtvrté, každý týden určete jednoho inženýra jako API reviewera. Ne žádného architektonického cara ani správce procesu, prostě člověka, který hlídá, že naming, shape chyb, styl paginace a deprecation metadata zůstávají konzistentní. V týmu o pěti nebo šesti lidech funguje rotace úplně v pohodě.
Za páté, logujte identifikátory consumerů. I jednoduché mapování API klíčů nebo domluva přes User-Agent stačí na to, abyste věděli, kdo ještě používá staré cesty. Týmy tohle přeskočí, pak při deprecation hádají, a hádání je přesně důvod, proč nakonec rok podporujete mrtvé verze.
Lehčí verze tohohle přístupu jsme používali na projektech, kde byl stack postavený na Next.js na edge, na Django plus PostgreSQL pro těžší business workflow, i na malých interních nástrojích s HTMX, které stejně potřebovaly stabilní server kontrakty, protože je jiné systémy scrapovaly nebo do nich postovaly data. Princip je pokaždé stejný: napište kontrakt, automaticky ho ověřujte a ať je breakage vždycky vědomé rozhodnutí. Jakmile API přestane driftovat pod rukama celému týmu, rewrite přestane vypadat tak lákavě.
