🤔 Zou GraphQL Anders Moeten Zijn voor Verschillende Gebruikers?
GraphQL is een interface om gegevens op te halen uit een bepaalde bron, waarbij de GraphQL-specificatie de vereisten voor die interface vastlegt. Zolang aan deze vereisten wordt voldaan, maakt het GraphQL niet uit hoe dit wordt bereikt. De GraphQL-server kan dan worden geïmplementeerd in JavaScript met behulp van promises, via een gelijktijdige architectuur op basis van Golang, gekoppeld aan een Excel-bestand, of wat dan ook — en al deze implementaties kunnen geldige realisaties van de GraphQL-specificatie zijn.

Hoe de engine van de server is geĂŻmplementeerd, is niet van belang voor de succesvolle uitvoering van een GraphQL-verzoek, want de interactie tussen client en server is altijd hetzelfde: er wordt een GraphQL-query ingediend met een gedefinieerde syntaxis, en het antwoord wordt ontvangen in JSON-formaat.
Wanneer ik zeg dat de implementatie niet belangrijk is, bedoel ik dat vanuit het perspectief van de gebruiker van de API, die simpelweg gegevens wil ophalen van de server. Hoe die gegevens zijn geproduceerd, is niet van belang.
Maar de situatie verandert voor de server-side developer die aan de API werkt, voor wie de implementatiedetails wél heel belangrijk zijn. Als ik mijn GraphQL-API in PHP code, doe ik mijn best om mijn API zo efficiënt mogelijk te laten werken en een zo elegant mogelijk architecturaal ontwerp te hebben, met gebruikmaking van de mogelijkheden die PHP biedt.

Daarmee ontstaat een mogelijk belangenconflict tussen de noodzaak om de API te beveiligen en de verwachte mogelijkheden van ontwikkelaars die aan de API werken. Die willen niet dat functies die door de onderliggende taal worden ondersteund hen worden ontnomen — zoals de mogelijkheid om recursieve code uit te voeren.
Dit conflict werd duidelijk in issue #929: Allow recursive references in fragments, dat stelt dat GraphQL geen recursie in fragmenten zou moeten verbieden.
Op een eerdere meetup van de GraphQL working group uitte Roman — de developer die het issue indiende — zijn onenigheid met de beperking die de specificatie oplegt:
Ik ben een server-side developer, en ik heb het gevoel dat de specificatie te veel spreekt over server-side uitvoering, terwijl het zich zou moeten richten op wat de client wil ontvangen — niet hoe
De regel die recursie in fragmenten verbiedt, is gerechtvaardigd op basis van het handhaven van de veiligheid van de publieke API. GraphQL is immers door Facebook gemaakt om gegevens te leveren aan hun publiekgerichte applicatie, en gebruikers mogen geen gebruik kunnen maken van een fout in het API-ontwerp die de service zou kunnen platleggen.
GraphQL-bedenker Lee Byron uitte drie belangrijke bezwaren:
oneindige recursie; de beperkingen zouden niet alleen in de specificatie zitten — hoe en wanneer zou het moeten stoppen
gegevensvalidatie; dezelfde waarde meerdere keren teruggeven — hoe wordt dat weergegeven in de data. Idealiter wil je detecteren dat het cyclisch is en meteen stoppen, maar sommige servers kunnen dit niet detecteren en kunnen de lus meerdere keren doorlopen voordat ze merken dat er iets mis is en stoppen
wat zijn de kosten van het niet hebben hiervan; rechtvaardigt het deze problemen? Nee; het is altijd mogelijk om het aantal niveaus diepte in je query op te geven — dat is feitelijk de onsuikerde versie van wat we zouden doen als we dit in GraphQL zouden afhandelen
Beide vanuit hun eigen perspectief hebben Roman en Lee gelijk. Lee Byron maakt zich zorgen over de veiligheid van de publieke GraphQL-API. Het vermijden van recursieve fragmenten is gerechtvaardigd om te voorkomen dat kwaadwillenden het systeem kunnen platleggen door een eindeloze cyclische lus in de query uit te voeren, en zelfs om de kans op "self-DDoSing" door een team weg te nemen — iets dat kan gebeuren als ze per ongeluk een query publiceren die het systeem laat vastlopen.
Roman daarentegen is bezorgd over de beperkingen van zijn eigen mogelijkheden om een GraphQL-API te bouwen. Omdat Roman misschien de enige gebruiker van zijn API is (d.w.z. een privé-API die niet aan gebruikers wordt blootgesteld), of omdat zijn server de mogelijkheid heeft om herhalende cycli te detecteren en te stoppen, vindt hij de GraphQL-beperking schadelijk en niet gerechtvaardigd.
De kern van de discussie is niet of recursieve fragmenten al dan niet toegestaan moeten zijn, maar iets fundamentelers: Wie is de doelgroep van GraphQL? Als dat niet één groep is, kan één API-specificatie dan voldoen aan de vereisten van alle verschillende belanghebbenden? En als het conflict niet voorkomen kan worden, kan het dan tenminste enigszins worden verholpen?
Laten we deze vragen onderzoeken.
Wie is de doelgroep van GraphQL?
GraphQL wordt gebruikt door verschillende soorten belanghebbenden, waaronder we kunnen identificeren:
1. API-gebruikers: Degenen die gegevens ophalen uit een GraphQL-endpoint, om welke reden dan ook. We kunnen bijvoorbeeld allemaal gebruikers zijn van de publieke GraphQL-API van GitHub, om gegevens over onze GitHub-repos op te halen.
2. Client-side developers: Degenen die client-side applicaties bouwen die worden aangedreven door een GraphQL-endpoint. Zo vertrouwen developers die sites bouwen met Gatsby op GraphQL om de inhoud van de site op te halen.
3. Backend-developers: Degenen die de resolvers voor de GraphQL-API bouwen.
Daarnaast moeten we opmerken dat de GraphQL-API publiek of privaat kan zijn:
Publieke API: Omdat iedereen toegang heeft tot het GraphQL-endpoint, moeten we ons bezighouden met beveiligingsmaatregelen om aanvallen van kwaadwillenden te voorkomen.
Private API: Omdat alleen bevoegde partijen toegang hebben tot de API, zijn er geen inherente beveiligingsrisico's, en kan self-DDoSing gemakkelijk worden voorkomen met goede codeerpraktijken.
Voldoet één API-specificatie aan de vereisten van alle belanghebbenden?
Het issue dat Roman heeft aangekaart, kan als volgt worden geĂŻnterpreteerd: "Als mijn GraphQL-API privaat is, en ik weet precies wat ik doe (met 100% zekerheid dat mijn code werkt zoals verwacht en er geen vastlopende uitvoeringen worden geproduceerd), waarom kan ik dan geen recursie in fragmenten gebruiken?"

Een voorbeeld van deze situatie doet zich voor wanneer je een op GraphQL gebaseerd framework gebruikt voor het bouwen van statische sites (zoals Gatsby, Next.js of RedwoodJS), omdat de GraphQL-API dan vaak privaat is en je je applicatie niet per ongeluk kunt DDoS'en met nadelige gevolgen (in het ergste geval crasht de build van de statische site in een ontwikkelings- of staging-omgeving).
Developers die deze opzet gebruiken, mogen zich terecht afvragen waarom de GraphQL-specificatie hen verbiedt voordelige functies te gebruiken die voor hun opzet helemaal geen nadelige gevolgen hebben.
Kortom, door recursieve fragmenten te verbieden, legt de GraphQL-specificatie een beveiligingsmaatregel op die van toepassing is op een selectie van alle mogelijke toepassingen van GraphQL — niet alle — om aan de veilige kant te blijven.
Kan de GraphQL-specificatie alle belanghebbenden beter tevreden stellen?
Als verschillende belanghebbenden verschillende vereisten hebben, hoe kan de GraphQL-specificatie dan aan al hun wensen voldoen? (Het idee is om te voorkomen dat de specificatie wordt geforkt en er aangepaste versies voor specifieke doelgroepen worden gemaakt.)
Laten we een paar ideeën verkennen, waarbij het eerste door het specificatiebijdrageproces zou moeten gaan, en het tweede niet.
Feature-toggle op het niveau van de GraphQL-specificatie
Een mogelijke aanpak is om de specificatie regels te laten "suggereren" in plaats van "opleggen". In dat geval zou de regel die recursie in fragmenten verbiedt sterk worden aangeraden, maar de functie zou toch worden geaccepteerd.
Deze oplossing zou de standaardstatus van recursieve fragmenten echter veranderen van "verplicht" naar "optioneel", wat twee negatieve gevolgen zou hebben:
- De API zou standaard onveilig zijn (het scenario dat Lee Byron wil vermijden)
- Het zou een breaking change opleveren, omdat een verboden query dan zou worden toegestaan
Het is dan beter om de optie om te draaien: recursie in fragmenten standaard nog steeds verboden laten, maar de mogelijkheid bieden om een feature-flag in te schakelen die dit gedrag uitschakelt. Omdat de functie expliciet moet worden uitgeschakeld, zal dit alleen worden gedaan door beheerders die weten wat ze doen.
Omdat de functie het meest waardevol is in bepaalde opstellingen, kunnen GraphQL-servers en -frameworks beslissen of/hoe/wanneer ze de configuratie aanbieden. Gatsby zou de optie bijvoorbeeld prominent kunnen weergeven via een gebruikersinterface bij het aanmaken van statische sites, en deze anders verbergen.
Het algemene idee is dat de GraphQL-specificatie "ingeschakelde maar optionele functies" ondersteunt, die via configuratie in- of uitgeschakeld kunnen worden, waarbij hun standaardstatus de huidige toestand in de specificatie is.
Het verbieden van recursieve fragmenten zou er een van zijn, en er zouden andere zulke functies kunnen zijn, zoals een Map-type, dat niet werd geaccepteerd voor de specificatie door Lee Byron omdat:
Er zijn aanzienlijke afwegingen bij een Map-type versus een lijst van sleutel/waarde-paren. Een probleem is paginering over de collectie. Lijsten van waarden kunnen duidelijke pagineringsregels hebben, terwijl Maps — die vaak ongeordende sleutel-waarde-paren hebben — veel moeilijker te pagineren zijn.
Een ander probleem is het gebruik. Heel vaak wordt Map gebruikt in API's waarbij één veld van de waarde wordt geïndexeerd, wat naar mijn mening een API-antipatroon is, omdat indexering een opslag- en client-cachingkwestie is, maar geen transportkwestie. Dit antipatroon baart me zorgen. Hoewel er goede toepassingen zijn voor Maps in API's, vrees ik dat het gangbare gebruik voor deze antipatronen zal zijn, dus ik stel voor voorzichtig te werk te gaan.
Lee Byron uitte zijn vrees dat de functie als antipatroon zal worden gebruikt. Hij erkende echter ook dat er goede toepassingen voor zijn. Toen het issue veel steun kreeg van de community (met meer dan 150 👍), zouden developers de optie kunnen krijgen om het toevoegen van een Map-type aan hun schema's expliciet in te schakelen en de gevolgen te dragen.
Feature-toggle door GraphQL-servers
Als het bovenstaande voorstel geen steun krijgt omdat het te riskant is voor de GraphQL-specificatie, is het alternatief om dit op het niveau van de GraphQL-server te implementeren. GraphQL-servers zouden dan een aangepaste functie kunnen bieden die recursie in fragmenten uitschakelt.
Om het idee te veralgemenen: GraphQL-servers zouden kunnen aanbieden om bepaalde functies uit de specificatie uit te schakelen en andere in te schakelen die ontbreken in de specificatie. Om te voorkomen dat dit gedrag voor verrassingen zorgt, moeten de servers ervoor zorgen dat de standaardstatus die is welke de specificatie vereist, en de beheerder van de API moet volledig op de hoogte zijn van de gevolgen van het in- of uitschakelen van de functie. (Dit is de strategie die Gato GraphQL volgt voor zijn "innovative features".)
Samenvatting
Nu GraphQL steeds populairder is geworden, hebben nieuwe frameworks die nieuwe mogelijkheden ondersteunen het in hun stack opgenomen, en zijn er nieuwe belanghebbenden (en nieuwe typen daarvan) bij betrokken geraakt. Een specificatie die oorspronkelijk door Facebook werd gemaakt om te definiëren hoe zijn applicaties gegevens van zijn servers zouden ophalen, moet steeds meer omgaan met meer gebruikssituaties.
Het is onvermijdelijk dat er conflicten ontstaan, waarbij een groep belanghebbenden een functie nodig heeft die contraproductief of zelfs schadelijk is voor andere belanghebbenden, zoals bij recursieve fragmenten. Wat kan er worden gedaan om de situatie te verbeteren en te voorkomen dat ontevreden belanghebbenden teleurgesteld raken in GraphQL?
Ik heb betoogd dat de specificatie de mogelijkheid zou kunnen bieden om een functie "uit te schakelen", zodat beheerders die weten wat ze doen bepaalde beperkingen kunnen opheffen om aan hun eigen vereisten te voldoen. Ik ben het zelf niet eens met deze oplossing, maar ik breng het toch ter sprake omdat deze discussie gevoerd moet worden. Omdat dit idee controversieel is, is een beter alternatief dat GraphQL-servers dit gedrag bieden via aangepaste functies die expliciet moeten worden ingeschakeld.