GraphQL: ce este, cum funcționează, capcanele clasice
GraphQL este un query language pentru API-uri creat de Facebook în 2015. Clientul cere exact câmpurile vrute. Cum funcționează, N+1, și când să nu îl folosești.
Cuprins
GraphQL este un query language pentru API-uri creat de Facebook în 2012 și publicat ca open source în 2015. Clientul descrie exact câmpurile de care are nevoie; serverul răspunde cu strict acel șablon, nimic în plus. Toate cererile ajung la un singur endpoint, de obicei /graphql, prin POST.
Confuzia frecventă este că GraphQL ar fi un concurent direct al REST. Nu este. REST este un stil arhitectural; GraphQL este un limbaj de interogare. Poți construi un server GraphQL deasupra unui set de resurse REST sau deasupra unui Postgres. Decizia nu este ori-ori, ci depinde de cine consumă datele și cât de variată este forma cerută.
Ce este GraphQL mai exact?
GraphQL definește trei tipuri de operații, toate descrise într-o schemă strict tipizată scrisă în SDL (Schema Definition Language):
- Query (citire). Clientul cere date, descrie câmpurile vrute și primește exact acel șablon JSON. Nicio câmpuri extra față de ce a cerut.
- Mutation (scriere). Echivalentul
POST/PUT/DELETEdin REST. Modifică date pe server și returnează câmpurile selectate din obiectul afectat. - Subscription (real-time). O conexiune persistentă, de obicei peste WebSocket, care împinge date clientului când starea se schimbă pe server. Folositoare pentru notificări sau feed-uri live.
Schema SDL este un contract public între server și client. Declară tipuri (type Article { id: ID! title: String! author: User! }), câmpuri disponibile și tipul de returnare al fiecărei operații. Schema servește și ca documentație live: oricine poate inspecta schema printr-un mecanism numit introspection și știe exact ce poate cere. Avantaj major față de un API REST nedocumentat. Dezavantaj când introspection-ul este lăsat activ în producție, despre care mai jos.
Cum funcționează un query și unde costă mai mult decât REST?
Un query GraphQL tipic arată astfel:
query {
articles(limit: 10) {
id
title
author {
name
email
}
}
}
Serverul primește textul, îl parsează, validează față de schemă și execută resolver-ele pentru fiecare câmp. Rezolver-ele sunt funcții care știu cum să aducă un câmp concret, de obicei dintr-o bază de date sau dintr-un alt serviciu. Problema apare când rezolverul pentru câmpul author face un SELECT separat per articol. La zece articole, obții 11 query-uri în Postgres în loc de 2. La o sută de articole, 101. Acesta este N+1.
Soluția standard este DataLoader (bibliotecă lansată de Facebook): grupează toate cererile pentru câmpul author acumulate într-un singur ciclu de execuție și le rezolvă printr-un singur SELECT ... WHERE id IN (...). Fără DataLoader, un server GraphQL pe date relaționale este de obicei mai lent decât echivalentul REST cu endpoint-uri dedicate, nu mai rapid.
Care sunt capcanele clasice ale unui server GraphQL în producție?
- N+1 fără DataLoader. Descris mai sus. Este cel mai frecvent motiv pentru care un server GraphQL la primele versiuni rulează mai lent decât se așteptau echipele care l-au ales. Verifică cu un profiler de query-uri înainte de lansare.
- Introspection activ în producție. Introspection expune schema completă oricui face o cerere. Un atacator poate cartografia toate relațiile, câmpurile deprecate și tipurile interne fără autentificare. Dezactivează-l sau restricționează-l la sesiuni cu token JWT valid. Monitorizează cererile de introspection în stack-ul de observabilitate ca semnal de reconnaissance.
- Query-uri fără limitare de complexitate. Un query GraphQL recursiv (tipuri care se referă unele la altele) poate genera un plan de execuție exponential. Fără cost analysis la parse-time și fără depth limiting, un singur request rău intenționat poate doborî serverul. Rate limiting la nivel de rețea nu este suficient, pentru că problema este complexitatea unui singur request, nu frecvența.
- GraphQL ca proxy direct la baza de date. Instrumente ca Hasura sau PostGraphile generează automat un strat GraphQL deasupra Postgres. Sunt rapide de pus în producție, dar expun structura internă a bazei de date, incluzând relații pe care nu ai intenționat să le faci publice. Adaugă un strat de autorizare explicit înainte de a expune orice tablou.
- Absența persisted queries. Fără persisted queries, orice client poate trimite orice text de query, inclusiv query-uri costisitoare sau explorative. Persisted queries reduc și payload-ul: clientul trimite un hash, serverul execută query-ul stocat în registru. Reduce atât latența, cât și suprafața de atac.
Cum verifici performanța unui server GraphQL în producție?
Trei mecanisme de bază, aplicate în ordine de prioritate:
Cost analysis: la parse-time, înainte de execuție, calculezi un scor de complexitate pentru query. Fiecare câmp are un cost, câmpurile de tip listă multiplică costul copiilor. Dacă scorul depășește pragul configurat, respecți cu un mesaj explicit, nu consumi resurse. Bibliotecile mature (graphql-java, graphene) au implementări de cost analysis incluse.
Depth limiting: refuzi query-uri cu nesting mai adânc de un număr de niveluri (uzual 5-10). Un query cu zece niveluri de nesting anidic este aproape întotdeauna o eroare sau un atac, nu o cerere legitimă. Combini depth limiting cu cost analysis pentru o apărare mai solidă.
Metrici per operație: spre deosebire de REST unde fiecare endpoint are un URL distinct, în GraphQL toate cererile ajung la /graphql. Agregarea latentei și ratei de erori per operație numită (fiecare query sau mutation are un câmp operationName) îți arată care operații costă. Exportă metrics la nivel de operație în stack-ul de observabilitate; fără asta, un query lent se ascunde în media generală a endpoint-ului.
Când să NU folosești GraphQL?
GraphQL adaugă complexitate reală: un server de schema, resolver-e per câmp, DataLoader pentru relații, cost analysis, persisted queries pentru producție. Complexitatea se justifică când ai consumatori cu nevoi radical diferite de date sau când un produs cu UI extrem de flexibil cere combinații variate din același set de câmpuri.
Nu se justifică în câteva scenarii frecvente. Când consumi API-ul dintr-un singur tip de client cu UI fix, REST cu endpoint-uri dedicate și documentație OpenAPI este mai simplu de menținut și mai ușor de protejat. Când ai nevoie de caching HTTP nativ la CDN sau gateway, REST câștigă clar: GET /articles/123 se poate cache la nivelul rețelei fără configurație suplimentară, în timp ce orice cerere GraphQL este POST și nu se cache-ează implicit. Când echipa nu are experiență cu DataLoader și cost analysis, costul de a pune GraphQL în producție în siguranță este deseori subestimat la start.
Noi am ales REST pentru produsele proprii și pentru proiectele B2B pe care le livrăm clienților. Motivele sunt operaționale, aceleași pe care le descriem în intrare dedicată despre alegerea între REST și GraphQL: tooling mai matur, depanare prin curl, caching HTTP fără infrastructură adăugată și faptul că Spring Boot @RestController din crawlerra-backend este standardul natural al stivei noastre Java. Alegerea se schimbă dacă cerințele se schimbă; nu există un răspuns universal.
Întrebări frecvente
GraphQL înlocuiește REST?
Nu, sunt instrumente cu filozofii diferite și fiecare câștigă în alt context. REST este un stil arhitectural bazat pe resurse și verbe HTTP; GraphQL este un query language care lasă clientul să definească forma răspunsului. Poți folosi ambele în același sistem dacă ai un motiv clar pentru fiecare strat.
Ce este problema N+1 în GraphQL?
Un query GraphQL care cere o listă de articole plus autorul fiecăruia execută implicit 1 query pentru articole și N query-uri pentru autori, câte unul per rând. La 100 de articole, ajungi la 101 interogări în baza de date în loc de 2. Soluția standard este DataLoader, care grupează cererile per ciclu de execuție și le rezolvă printr-un singur SELECT cu IN.
Introspection trebuie dezactivat în producție?
Da, în mod implicit. Introspection permite oricui să descarce schema completă a serverului GraphQL, inclusiv câmpurile deprecate și relațiile interne. Dezactivează-l sau restricționează-l la clienți autentificați. Monitorizează cererile de introspection în producție pentru a detecta tentative de cartografiere a API-ului.
Cum se protejează un server GraphQL de query-uri abuzive?
Prin cost analysis și depth limiting aplicate înainte de execuție, nu prin rate limiting la nivel de rețea. Un query GraphQL recursiv sau cu nesting profund poate supraîncărca serverul fără să atingă un endpoint explicit. Rate limiting per IP nu ajunge, pentru că un singur request poate consuma resurse disproporționat de mari.
Persisted queries ajută la performanță?
Da, în două feluri: reduc payload-ul transferat și blochează query-urile ad-hoc în producție. Clientul trimite un hash al query-ului, serverul caută hash-ul într-un registru local și execută textul original stocat. Clienții neautorizați nu pot construi query-uri arbitrare, ceea ce reduce suprafața de atac considerabil.