Cíle a principy návrhu GraphQL schématu
GraphQL schéma je kontrakt mezi klienty a serverem. Definuje tvary dat (typy), způsob jejich čtení (Query), změn (Mutation) a událostní proudy (Subscription). Návrh schématu a resolverů mají řídit potřeby domény, nikoli fyzický model databáze. Klíčové charakteristiky kvalitního návrhu jsou: explicitní typy, jasná nulovatelnost, stabilní identita uzlů, dobře definované hranice (argumenty, filtry, stránkování) a pozorovatelnost (telemetrie, chybové kódy, latence).
Doménové modelování: od Ubiquitous Language k SDL
- Ubiquitous Language: nejprve popište subdomény, entity a jejich vztahy jazykem byznysu. Vyhněte se databázovým pojmům v rozhraní.
- Agregáty a hranice: určete, které uzly mají globální identitu (např.
User,Order) a které jsou hodnotovými objekty (Money,Address). - GraphQL SDL jako kontrakt: zapisujte typy s popisky (
""" … """) a direktivami pro nástroje (např.@deprecated).
Základní stavební prvky schématu: typy, rozhraní, unie
- Objektové typy: reprezentují doménové entity. Příklad:
type User { id: ID!, email: String!, name: String, roles: [Role!]! } - Rozhraní (Interfaces): pro polymorfismus a sdílená pole. Příklad:
interface Node { id: ID! } - Unie (Union): pro variantní návratové hodnoty bez společných polí. Příklad:
union SearchResult = User | Organization | Article - Vlastní skalární typy:
scalar DateTime,scalar URL; vždy specifikujte formát a validaci. - Vstupní typy: jsou určeny pro argumenty, aby byly stabilní a rozšiřitelné. Příklad:
input UserFilter { email: String, role: Role, createdFrom: DateTime }
Nulovatelnost a kontrakt kvality dat
Operátor ! vyjadřuje, že hodnota nesmí být null. Používejte jej uvážlivě: přísná nulovatelnost zvyšuje spolehlivost klientů, ale vyžaduje pečlivé strategie resolverů a deterministické chování při chybách. Držte se zásady: non-null pro identitu a klíčová pole, nullable pro volitelné a odvozené hodnoty.
Kořenové operace: Query, Mutation, Subscription
- Query: navrhujte jako čtení orientované na konkrétní případy použití (use case) (např.
userById(id: ID!): User,search(query: String!, first: Int, after: Cursor): SearchConnection!). - Mutation: používejte slovesa označující příkazy (command) a vstupní typy (input) (
createUser(input: CreateUserInput!): CreateUserPayload!) pro stabilitu a možnost rozšíření oclientMutationId. - Subscription: explicitně popište sémantiku streamu a filtry (např.
orderStatusChanged(orderId: ID!): OrderStatusEvent!); dbejte na autorizaci.
Stránkování a kolekce: offset vs. kurzory
- Offset: jednoduchý pro malé, stabilní seznamy; citlivý na změny pořadí.
- Stránkování podle kurzoru (Relay Connection):
edges { node, cursor },pageInfo { hasNextPage, endCursor }; odolnější vůči změnám v čase. Doporučuje se pro velké seznamy. - Filtry a řazení: udržujte
filteraorderByjako vstupní typy (input); umožníte tak jejich vývoj bez narušení kontraktu.
Architektura resolverů: vrstvy a odpovědnosti
- Čisté resolvery: tenké adaptéry bez business logiky; orchestrují přístup ke službám a mapují data do tvaru schématu.
- Servisní vrstva: doménové služby, které řeší pravidla a transakce; lze je znovu použít i mimo GraphQL.
- DataLoader/hromadné načítání: eliminace dotazů N+1 agregací požadavků podle klíče a ukládáním do mezipaměti na úrovni požadavku.
- Kontext: injektujte uživatele, tenanta, jazykové nastavení a trace ID; nepoužívejte mezipaměť specifickou pro požadavek s globálním dopadem.
Eliminace problému N+1 a strategie načítání
- DataLoader pro každý požadavek: dávkově načítejte podle ID a ukládejte výsledky do mezipaměti po dobu jednoho dotazu.
- Projekce a výběr polí: přenášejte do datové vrstvy informaci o požadovaných polích (např. výběr polí → projekce SQL).
- Joiny vs. následné dotazy: u malých vztahů se vyplatí join/projekce; pro velké seznamy upřednostněte stránkování podle kurzoru.
Model chyb a návratové payloady
- Částečný úspěch: GraphQL umožňuje vrátit data i s chybami; proto navrhujte deterministickou nulovatelnost a chyby v
errorsse strojově čitelnýmextensions.code. - Payload mutace: vracejte
{ success: Boolean!, user: User, errors: [UserError!]! }s polem pro chyby validace (na úrovni polí). - Business kódy: standardizujte
extensions.code(např.UNAUTHORIZED,FORBIDDEN,NOT_FOUND,CONFLICT).
Autentizace a autorizace
- Autentizace v kontextu: ověřte token a načtěte identitu a role dříve, než se spustí resolvery.
- Autorizace jako knihovna: zajistěte, aby resolvery volaly politiky (ABAC/RBAC), nebo použijte direktivy (např.
@auth(role: ADMIN)) napojené na validační logiku. - Ochrana na úrovni polí: přístup k citlivým polím (např.
User.email) kontrolujte v resolveru daného pole, ne pouze v kořenovém resolveru.
Vývoj schématu a kompatibilita
- Rozšiřování bez narušení kompatibility: přidávejte volitelná pole, nové typy a nové unie; nepřidávejte
!tam, kde dříve nebyl. - Označení za zastaralé: používejte
@deprecated(reason: "...")a zveřejňujte plán odstranění. - Verzování: upřednostňujte evoluční vývoj v rámci jednoho endpointu; endpoint v2 vytvářejte pouze při zásadní refaktorizaci.
Federace a modulární schéma
- Propojování schémat / federace: rozdělte monolit na doménové podgrafy (Users, Orders, Catalog) a skládejte je přes gateway; dbejte na globální identitu a reference.
- Kontrakty mezi týmy: každý tým vlastní část schématu; gateway vynucuje kompozici, limity a pozorovatelnost.
Výkon, mezipaměť a perzistentní dotazy
- Perzistentní dotazy: připněte dotazy identifikované pomocí hashů; snížíte tak riziko DoS prostřednictvím textu dotazu a zlepšíte TTFB.
- Vrstvy mezipaměti: mezipaměť odpovědí pro idempotentní dotazy, mezipaměť objektů (např. podle
id), mezipaměť na okraji sítě sCache-Controlpro operace označené jako@cacheable. - Limity a složitost: vypočítejte náročnost dotazu (hloubku, násobitele seznamů), zaveďte omezení počtu požadavků a časové limity pro jednotlivé resolvery.
Bezpečnost a odolnost
- Validace vstupů: validujte délky řetězců, formáty a číselné rozsahy; chraňte se proti Regex DoS a hluboké rekurzi.
- Aliasy a fragmenty: omezte počet aliasů, hloubku fragmentů a cyklické reference.
- Pozorovatelnost: trace ID v kontextu, metriky latencí a počtu volání resolverů, vzorkování a strukturované logy s poli
operationNameavariables(bez tajných údajů).
Testování a kvalita
- Jednotkové testy resolverů: simulujte služby a kontext; testujte autorizaci a chybové větve.
- Integrační testy operací: spouštějte skutečné GraphQL dotazy proti testovacímu serveru s databází v paměti.
- Kontraktní testy: ověřujte, že publikované SDL odpovídá očekáváním klientů; využijte registr schémat a kontrolu nekompatibilních změn v CI.
Praktický návrhový vzor pro mutace
Pro každou mutaci používejte dvojici input a payload, aby bylo možné snadno přidávat pole, aniž by se narušila funkčnost klientů. Příklad: input CreateUserInput { email: String!, name: String, role: Role = USER } a type CreateUserPayload { success: Boolean!, user: User, errors: [UserError!]! }.
Ukázkový výřez SDL pro blogovou doménu
interface Node { id: ID! }
type User implements Node { id: ID!, email: String!, name: String }
type Post implements Node { id: ID!, title: String!, body: String!, author: User! }
input PostFilter { authorId: ID, query: String }
type PostEdge { node: Post!, cursor: String! }
type PostConnection { edges: [PostEdge!]!, pageInfo: PageInfo! }
type PageInfo { hasNextPage: Boolean!, endCursor: String }
type Query { post(id: ID!): Post, posts(first: Int = 20, after: String, filter: PostFilter): PostConnection! }
input CreatePostInput { title: String!, body: String! }
type CreatePostPayload { success: Boolean!, post: Post, errors: [UserError!]! }
type Mutation { createPost(input: CreatePostInput!): CreatePostPayload! }
Resolvery: vzorce chování
- Kořenové resolvery (
Query/Mutation): validujte vstupy, kontrolujte oprávnění a delegujte na doménové služby. - Resolvery polí (např.
Post.author): používejte DataLoader pro hromadné načítání autora pomocíloadManypodleauthorId. - Resolvery spojení: implementujte kurzory jako bezpečné, neprůhledné hodnoty (např. base64 s
id|sortKey) a vracejte konzistentnípageInfo.
Lokalizace, měny a jednotky
- Jazykové nastavení v kontextu: předávejte jazyk a časové pásmo prostřednictvím kontextu a respektujte je v resolverech.
- Typy pro hodnoty: místo
Floatpoužívejte strukturované typy (Money { amount: Decimal!, currency: Currency! }) a pevně definované jednotky (Length,Weight).
Ověřitelnost a dokumentace
- Popisy (docstrings): u každého typu a pole sjednoťte terminologii a uvádějte příklady.
- Registr schémat a changelog: zveřejňujte SDL, sledujte změny a rozesílejte upozornění při možných nekompatibilních změnách.
- Prozkoumávání: povolte GraphiQL/Explorer pouze v bezpečných prostředích; v
variablesmaskujte tajné údaje.
Kontrolní seznam pro závěrečnou revizi schématu
- Jsou ID stabilní a globálně jedinečná? Je rozhraní
Nodeimplementováno konzistentně? - Je nulovatelnost konzistentní a odpovídá skutečnému chování?
- Mají kolekce stránkování podle kurzoru a definované řazení?
- Jsou mutace idempotentní tam, kde je to žádoucí, a vracejí payload s chybami?
- Nemají resolvery problémy N+1 a využívají dávkové načítání a metriky?
- Jsou autorizace a limity požadavků vynucovány ve správných vrstvách?
Závěr: navrhněte kontrakt pro lidi, ne pro databázi
Úspěšné GraphQL schéma odráží doménu a potřeby klientů, nikoli fyzické tabulky. Dobře navržené typy, disciplinovaná nulovatelnost, promyšlené stránkování, čisté resolvery s dávkovým načítáním, robustní model chyb a průběžná pozorovatelnost vedou ke stabilnímu, výkonnému a bezpečnému API, které se může vyvíjet bez narušení funkčnosti klientů.
