Návrh GraphQL schématu a resolverů: implementační postup

Cíle a principy návrhu GraphQL schématu

GraphQL schéma je kontrakt mezi klienty a serverem. Definuje struktury dat (typy), způsoby jejich dotazování (Query), modifikace (Mutation) a eventové toky (Subscription). Návrh schématu a resolverů by měl vycházet z potřeb domény, nikoliv z fyzického modelu databáze. Klíčovými atributy kvalitního návrhu jsou: explicitní typy, jasná nulovatelnost, stabilní identita entit, 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 v terminologii byznysu. Vyhněte se databázové terminologii ve veřejném rozhraní.
  • Agregáty a hranice: určete, které uzly mají globální identitu (například User, Order) a které jsou hodnotové objekty (Money, Address).
  • GraphQL SDL jako kontrakt: definujte typy s popisky (""" … """) a direktivami pro nástroje (například @deprecated).

Základní stavební bloky schématu: typy, rozhraní, unie

  • Object typy: reprezentují doménové entity. Příklad: type User { id: ID!, email: String!, name: String, roles: [Role!]! }
  • Rozhraní (Interfaces): slouží pro polymorfismus a sdílené vlastnosti. Příklad: interface Node { id: ID! }
  • Unie (Union): umožňují variantní návraty bez společných polí. Příklad: union SearchResult = User | Organization | Article
  • Vlastní skaláry: například scalar DateTime, scalar URL; vždy specifikujte formát a validaci.
  • Input typy: 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ě: striktní nulovatelnost zvyšuje spolehlivost klientů, ale vyžaduje pečlivé resolver strategie 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á pole.

Kořenové operace: Query, Mutation, Subscription

  • Query: navrhujte je jako use-case orientované čtení (například userById(id: ID!): User, search(query: String!, first: Int, after: Cursor): SearchConnection!).
  • Mutation: používejte příkazová slovesa a input typy (createUser(input: CreateUserInput!): CreateUserPayload!) pro zachování stability a možnost rozšiřování o clientMutationId.
  • Subscription: explicitně definujte sémantiku streamu a filtry (např. orderStatusChanged(orderId: ID!): OrderStatusEvent!); dbejte na autorizaci.

Stránkování a kolekce: offset vs. kurzory

  • Offset: jednoduchý způsob vhodný pro malé a stabilní seznamy; však citlivý ke změnám pořadí položek.
  • Cursor-based (Relay Connection): s strukturou edges { node, cursor } a pageInfo { hasNextPage, endCursor }; odolnější vůči mutacím během zpracování. Doporučeno pro velké seznamy.
  • Filtry a řazení: udržujte filter a orderBy jako input typy, což umožní evoluci rozhraní bez narušení kontraktu.

Resolver architektura: vrstvy a odpovědnosti

  • Čisté resolvery: tenké adaptéry bez business logiky; orchestrují přístup ke službám a mapují data do formátu schématu.
  • Servisní vrstva: doménové služby řešící pravidla a transakce; použitelné i mimo GraphQL kontext.
  • DataLoader/batching: eliminace N+1 problému agregací požadavků podle klíče a caching na úrovni jednoho dotazu.
  • Kontext: injektujte uživatele, tenant, locale, trace ID; nikoliv cache s globálním dopadem specifickou pro request.

Eliminace N+1 problému a strategie načítání

  • DataLoader na dotaz: batchujte dotazy podle ID a cacheujte výsledky v rámci jednoho požadavku.
  • Projekce a selekce polí: předávejte do datové vrstvy informaci o požadovaných polích (např. výběr polí → SQL projekce).
  • Joiny vs. následné dotazy: u malých relací se vyplatí spojování/projekce; u velkých seznamů preferujte kurzorové stránkování.

Chybový model a návratové payloady

  • Částečný úspěch: GraphQL umožňuje vrátit data i s chybami; proto navrhujte deterministickou nullabilitu a chyby v errors se strojově čitelným extensions.code.
  • Mutation payload: vracejte objekt { success: Boolean!, user: User, errors: [UserError!]! } s polem pro validaci na úrovni polí.
  • Business kódy: standardizujte hodnoty extensions.code (například UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT).

Autentizace a autorizace

  • Autentizace v kontextu: ověřte token, načtěte identitu a role před spuštěním resolverů.
  • Autorizace jako knihovna: nechte resolvery volat politiky (ABAC/RBAC) nebo použijte direktivy (např. @auth(role: ADMIN)) mapované na validační logiku.
  • Ochrana na úrovni polí: citlivá pole (například User.email) kontrolujte v resolvere pole, ne pouze na kořenové úrovni.

Evoluce schématu a kompatibilita

  • Rozšiřování bez porušení kompatibility: přidávejte volitelná pole, nové typy, nové unie; nepřidávejte ! tam, kde dříve nebyl.
  • Deprecation: používejte @deprecated(reason: "...") a zveřejňujte plán odstranění.
  • Verzování: preferujte postupnou evoluci v rámci jednoho endpointu; verzi v2 zaveďte pouze při zásadní refaktorizaci.

Federace a modulární schéma

  • Schema stitching / federation: rozdělte monolit na doménové subgrafy (Users, Orders, Catalog) a skládejte je přes gateway; dbajte na globální identitu a reference.
  • Kontrakty mezi týmy: každý tým spravuje část schématu; gateway vynucuje kompozici, limity a observabilitu.

Výkon, cache a perzistentní dotazy

  • Persisted Queries: ukládejte hashované dotazy; snižujete riziko DoS útoků pomocí textu dotazu a zlepšujete čas do první odpovědi (TTFB).
  • Cache vrstvy: response cache pro idempotentní dotazy, object cache (například per id), edge cache s hlavičkou Cache-Control pro @cacheable operace.
  • Limity a složitost: kalkulujte nákladnost dotazu (hloubka, multiplikátory pro seznamy), zaveďte rate limiting a timeouty na jednotlivé resolvery.

Bezpečnost a odolnost

  • Validace vstupů: kontrolujte délku řetězců, formáty, rozsahy čísel; chraňte se proti Regex DoS a hluboké rekurzi.
  • Alias a fragmenty: omezujte počet aliasů, hloubku fragmentů a cyklické reference.
  • Observabilita: trace-id v kontextu, metriky latence a počtu volání resolverů, sampling a strukturované logy s poli operationName a variables (bez citlivých údajů).

Testování a kvalita

  • Jednotkové testy resolverů: mockujte služby a kontext; testujte autorizaci a chybové scénáře.
  • Integrační testy operací: spouštějte skutečné GraphQL dotazy proti testovacímu serveru s in-memory databází.
  • Kontraktační testy: ověřujte, že zveřejněné SDL odpovídá očekáváním klientů; využijte schema registry a kontrolu breaking changes v CI pipeline.

Praktický návrhový vzor pro mutace

Používejte input a payload dvojici pro každou mutaci, aby bylo možné přidávat pole bez rozbití 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í, delegujte na doménové služby.
  • Field resolvery (například Post.author): používejte DataLoader pro dávkové načítání více autorů podle authorId.
  • Connection resolvery: implementujte kurzory jako bezpečné, neprůhledné (například base64 kódování id|sortKey), vracejte konzistentní pageInfo.

Lokalizace, měny a jednotky

  • Locale v kontextu: přenášejte jazyk a časové pásmo přes kontext a respektujte je v resolverech.
  • Typy pro hodnoty: místo Float používejte strukturální typy (Money { amount: Decimal!, currency: Currency! }) a pevné jednotky (Length, Weight).

Verifikovatelnost a dokumentace

  • Popisy (docstrings): u každého typu a pole sjednoťte terminologii a uveďte příklady použití.
  • Schema registry a changelog: publikujte SDL, sledujte změny a zasílejte upozornění na možné breaking changes.
  • Explorace: zpřístupněte GraphiQL/Explorer pouze v bezpečných prostředích; maskujte citlivá data ve variables.

Checklist pro finální revizi schématu

  • Jsou ID stabilní a globálně jedinečné? Je Node konzistentně implementováno?
  • Je nullability konzistentní a odpovídá reálnému chování?
  • Mají kolekce kurzorové stránkování a definované řazení?
  • Jsou mutace idempotentní tam, kde je to žádoucí, a vracejí payload s chybami?
  • Jsou resolvery odolné vůči N+1 problémům, s dávkováním a metrikami?
  • Jsou autorizace a rate limity vynuceny na správných úrovních?

Závěr: navrhněte kontrakt pro lidi, ne pro databázi

Úspěšné GraphQL schéma reflektuje doménu a potřeby klientů, nikoli fyzickou strukturu tabulek. Pečlivě navržené typy, disciplinovaná nullabilita, promyšlené stránkování, čisté resolvery s dávkováním, robustní chybový model a průběžná observabilita vedou k stabilnímu, výkonnému a bezpečnému API, které může být evolučně rozšiřováno bez narušení klientských aplikací.