10 min čteníJohnny UnarJohnny Unar

Ve 12 službách GraphQL gateway zařízněte

Federation přidává governance daň, kterou si většina startupů v mid-stage fázi nemůže dovolit. Typed OpenAPI a consumer-driven contracts vrátí rychlost a udělají chyby viditelné.

federation stárne špatně

GraphQL federation vypadá chytře u třetí služby, možná u čtvrté. Demo je působivé, každý tým má autonomii, produkt má jeden endpoint, frontend si řekne přesně o ta pole, která chce, všichni kývou, a za půl roku máte koordinační mašinu převlečenou za architekturu. Tenhle pattern jsem viděl už mockrát a ten sales pitch už nekupuju. Aspoň ne u startupů v mid-stage fázi, které jedou něco kolem deseti služeb, mají jeden platform tým a pár produktových squad, které se snaží shipovat reálné feature v reálných deadlinách.

Ta bolest není teoretická. Ta bolest je úterý odpoledne, někdo přidá nullable field do subgraphu, resolver pořád hází chybu na chybějícím join key, gateway se bez problému zkomponuje, staging projde, protože happy path query fungovala, a pak production začne vracet partial data a hromadu errors: [{ message: "Cannot return null for non-nullable field" }], které klient ignoruje, protože HTTP status je 200. To je špatný návrh systému. Objevování chyb jste přesunuli z compile time do distribuovaného runtime a celé jste to obalili protokolem, který spotřebitele svádí k tomu, aby si mysleli, že jsou v bezpečí, protože mají typy vygenerované ze supergraphu.

Schema governance se zvrhne rychle. Každá změna napříč týmy se změní v malý normalizační výbor: pojmenovávání argumentů, debaty o ownership entit, rozhodování, kam které pole patří, čekání na composition checky, replay query a pokusy pochopit N+1 chování, které je teď rozsekané přes několik resolver chainů. Gateway se promění v integrační džungli, místo, kde se vrství auth, caching, fanout, field-level joiny, převody shape a tribal knowledge, až do chvíle, kdy na to v pátek nikdo nechce sáhnout.

Spousta týmů si plete centralizaci query shape se zjednodušením systému. To není totéž. Jeden graph pořád může schovávat bordel, a v praxi ho skoro vždycky schovává.

náhrada postavená na kontraktech

Náhrada, kterou doporučuju, je záměrně nudná: OpenAPI 3.1 specifikace vlastněné po doménách, machine-generated klienti commitnutí v consumer repozitářích, consumer-driven contract testy, codegen v CI a velmi tenká edge vrstva, která řeší auth, rate limiting a request routing, ale netváří se jako nějaký orchestration mozek. Tohle má méně pohyblivých částí, lepší failure modes a výrazně čistší model ownershipu.

OpenAPI 3.1 konečně opravilo většinu historických důvodů, proč ho lidi shazovali. Zarovnání s JSON Schema je důležité, oneOf a discriminators jsou použitelné, nullable semantics přestaly být divné, examples a webhooks jsou first-class a tooling kolem TypeScriptu je o míle dál než před pár lety. Na Next.js frontendu nebo v React Native aplikaci generujeme klienty přes openapi-typescript a malý wrapper nad fetch, pak v CI pustíme tsc --noEmit, takže se rozbití consumera ukáže před merge, ne až po deploymentu.

Minimální setup vypadá takhle:

yaml
1openapi: 3.1.0
2info:
3 title: billing-api
4 version: 1.4.0
5paths:
6 /customers/{customerId}/invoices:
7 get:
8 operationId: listCustomerInvoices
9 parameters:
10 - name: customerId
11 in: path
12 required: true
13 schema:
14 type: string
15 format: uuid
16 responses:
17 '200':
18 description: OK
19 content:
20 application/json:
21 schema:
22 $ref: '#/components/schemas/InvoiceList'
23components:
24 schemas:
25 InvoiceList:
26 type: object
27 required: [items]
28 properties:
29 items:
30 type: array
31 items:
32 $ref: '#/components/schemas/Invoice'
33 Invoice:
34 type: object
35 required: [id, status, totalCents]
36 properties:
37 id: { type: string, format: uuid }
38 status: { type: string, enum: [draft, open, paid, void] }
39 totalCents: { type: integer }

Pak v CI:

json
1{
2 "scripts": {
3 "codegen": "openapi-typescript ./openapi/billing.yaml -o ./src/gen/billing.ts",
4 "typecheck": "tsc --noEmit",
5 "contracts": "pnpm pact:verify"
6 }
7}

Consumer tím získá statické garance vůči producer kontraktu a producer si pořád drží ownership implementačních detailů. Bez jakéhokoliv gateway composition divadla.

runtime chyby se přestanou schovávat

Nejsilnější argument pro tenhle přístup není o vkusu. Je o tom, kde se objeví chyby. Federation schovává příliš mnoho failů za úspěšný transport. Graph může vrátit 200 s půlkou stránky pryč, timeout resolveru může degradovat jednu větev query, zatímco zbytek projde, a klient musí číst errors[] i data zároveň, aby pochopil, co se vlastně stalo. Většina týmů tohle nemodeluje poctivě, jen pattern-matchuje na generated hooky nebo SDK metody a bere vyřešený promise jako validní data.

REST s explicitními kontrakty je mnohem méně magický, a právě proto se lépe provozuje. Když GET /customers/:id/invoices rozbije shape response, v consumer CI exploduje vygenerovaný TypeScript. Když producer změní chování, Pact verification spadne před deployem. Když route leží, dostanete 5xx a observability nástroje přesně vědí, co s tím. Error budgets, alerting, retries, chování CDN, cache keys, auth policies, tohle všechno už umí s HTTP pracovat bez přemlouvání.

Consumer-driven contract testy jsou důležité proto, že produceři jsou zoufale špatní v odhadu toho, na čem consumeři opravdu závisejí. Backend tým vám zcela upřímně řekne, že odstranění invoice.totalCents je safe, protože webová aplikace si umí spočítat zobrazované hodnoty z line itemů, a pak se ozve mobile, že to pole používá v offline view, a support dashboard má CSV export, který stojí na tom přesném integeru. Pact tohle chytí. Nám se osvědčilo ukládat pacty po consumerech, ověřovat je v provider pipeline a blokovat release, když verification spadne. Jednoduché pravidlo, žádné drama.

Jeden konkrétní pattern funguje dobře: breaking sémantické změny verzujte v path jen tehdy, když je to nutné, aditivní změny nechávejte na místě a deprecations zapisujte do specifikace i s daty. Lidé to přečtou. Tooling to vynutí. Nikdo nemusí v hlavě simulovat graph planner.

jak jsme zkrátili čas na změnu

Na jedné migraci ve steezr se stack o 12 službách dostal do klasického stavu: vpředu GraphQL gateway, za ní Django a Go služby, pár interních Node služeb na zpracování dokumentů, všude PostgreSQL, uprostřed Redis a frontend tým, který se naučil bát schema změn, protože každá netriviální feature překračovala hranice tří týmů. Mean time to change pro cokoliv, co se dotýkalo customer accounts, byl zhruba osm pracovních dní, někdy víc, a většina času padla na koordinaci, ne na kódování.

Nešli jsme do žádného dramatického přepisu. To by bylo nezodpovědné. Začali jsme inventurou reálného consumer trafficu z gateway logů, seskupili operace po doménách a napsali OpenAPI 3.1 specifikace pro patnáct nejdůležitějších workflow, které dělaly asi 80 % user-facing trafficu. Pak jsme před existující služby postavili tenkou HTTP edge vrstvu, Envoy na routing a propagaci auth kontextu, bez fancy BFF logiky, bez agregace, kromě několika endpointů, kde agregace byla skutečně stabilní a patřila do domény.

Klienti se generovali po consumerech. Web dostal čisté TypeScript typy a malý wrapper kolem fetch, native dostal stejný contract package přes shared workspace, interní nástroje na HTMX volaly endpointy napřímo, protože generované SDK nepotřebovaly. Do provider repozitářů jsme přidali contract verification joby. Consumer repozitáře pinovaly verze specifikací, regenerovaly v CI a failovaly hned při breaking change. Zjednodušený GitHub Actions job vypadal takhle:

yaml
1name: contract-check
2on: [pull_request]
3jobs:
4 verify:
5 runs-on: ubuntu-latest
6 steps:
7 - uses: actions/checkout@v4
8 - uses: pnpm/action-setup@v4
9 - uses: actions/setup-node@v4
10 with:
11 node-version: 22
12 cache: pnpm
13 - run: pnpm install --frozen-lockfile
14 - run: pnpm codegen
15 - run: pnpm typecheck
16 - run: pnpm contracts

Výsledek byl okamžitý. Mean time to change spadl během dvou měsíců na zhruba čtyři dny, hlavně proto, že frontend a backend se zase mohly hýbat nezávisle. Chyby byly viditelnější. Ownership byl jasnější. Práce na gateway přestala být specialist bottleneck.

námitky neobstojí

Standardní námitka je flexibilita frontendu: týmy chtějí ad hoc query shape, graph jim dává svobodu, REST vytváří endpoint sprawl. Tohle obvykle říkají týmy, které se nepodívaly poctivě na svůj traffic. Většina produkčních aplikací nemá nekonečnou variabilitu query, má malou sadu stabilních obrazovek a workflow, a ty se mapují docela čistě na resource nebo task-oriented endpointy. Jestli opravdu potřebujete custom projekce pro reportingový screen, přidejte sparse fieldsets nebo purpose-built read endpointy. Nestavte distribuovaný query planner pro celou firmu jen proto, že jeden dashboard chce pět joinů.

Další námitka je over-fetching. Jasně, existuje, ale skoro každý tým dramaticky přeceňuje jeho cenu ve srovnání s provozní cenou federation. Poslat o 1,5 KB JSONu navíc přes HTTP/2 nebo HTTP/3 je málokdy váš bottleneck. Skutečné bottlenecky jsou neomezený fanout přes subgraphy, skryté resolver waterfalls a query variabilita, která zabíjí cache. Radši vezmu o něco tlustší response než graph, kde potřebujete Apollo Router experty, aby vysvětlili, proč p95 po nenápadné schema změně vyskočilo ze 180 ms na 900 ms.

Pak je tu vyspělost toolingu. Lidi si myslí, že GraphQL tooling je příjemnější, protože introspection a codegen působí uhlazeně. OpenAPI tooling je v roce 2026 v pohodě, spíš víc než v pohodě, zvlášť pokud jsou vaši consumeři hodně na TypeScriptu. openapi-typescript, Redocly CLI, Stoplight Spectral, Pact, Dredd, pokud ho ještě chcete, Prism na mocking, tohle všechno funguje. Můžete lintovat specifikace, diffovat je v CI, generovat SDK, publikovat dokumentaci a vynucovat style rules. Na nic z toho nepotřebujete centrální platform tým v roli kněžstva.

Federation použijte tehdy, když opravdu máte hodně nezávislých domén, hodně nezávislých týmů a silnou platform skupinu, která bere graph governance jako produkt. Většina startupů tohle nemá. Má šest až patnáct služeb, jednu nebo dvě sdílené databáze, kterých se snaží zbavit, pár inženýrů v několika rolích najednou a žádný volný headcount na architektonickou marnivost.

rozumnější default

Můj default stack pro tenhle typ firmy je docela přímočarý: doménové HTTP API popsané v OpenAPI 3.1, generované TypeScript klienty používané v Next.js a React Native aplikacích, provider verification přes Pact, lint specifikací přes Spectral, codegen a tsc --noEmit v každém pull requestu, a na edge Envoy nebo NGINX na routing, auth a případně velmi lehké tvarování response tam, kde to dává smysl. Tohle odpovídá tomu, jak týmy reálně pracují. Stejně tak to odpovídá tomu, jak systémy selhávají v produkci, viditelně a debuggovatelně.

Praktické rozložení repozitáře pomůže hodně:

text
1/contracts
2 /billing/openapi.yaml
3 /accounts/openapi.yaml
4/services
5 /billing-api
6 /accounts-api
7/apps
8 /web
9 /mobile
10/packages
11 /api-clients

Držte kontrakty blízko kódu, publikujte vygenerované klienty jako workspace packages a breaking changes dělejte záměrně drahé. Dobrý CI gate je nudný: redocly lint, openapi-diff, openapi-typescript, tsc, Pact verification, hotovo. Když někdo změní response z totalCents: integer na total: string, automatizace by mu měla během pár minut jednu vrazit.

Tohle je jeden z těch architektonických callů, kde nuda vyhrává. Startupy v mid-stage fázi potřebují rychlost změn, předvídatelné chyby a systémy, kterým senior inženýr porozumí po hodině čtení repozitáře. Federation obvykle tlačí přesně opačným směrem. Typed OpenAPI plus consumer-driven contracts vás vrátí k systému postavenému kolem delivery, ne kolem ceremonie. A přesně tam měla většina týmů zůstat už od začátku.

Johnny Unar

Napsal/a

Johnny Unar

Chcete s námi spolupracovat?

Federation přidává governance daň, kterou si většina startupů v mid-stage fázi nemůže dovolit. Typed OpenAPI a consumer-driven contracts vrátí rychlost a udělají chyby viditelné.