Erstellen Ihrer ersten GraphQL-API

Veröffentlicht: 2022-03-11

Vorwort

Vor einigen Jahren führte Facebook eine neue Methode zum Erstellen von Back-End-APIs namens GraphQL ein, bei der es sich im Grunde um eine domänenspezifische Sprache für die Datenabfrage und -bearbeitung handelt. Anfangs habe ich dem nicht viel Aufmerksamkeit geschenkt, aber schließlich fand ich mich mit einem Projekt bei Toptal beschäftigt, wo ich Back-End-APIs basierend auf GraphQL implementieren musste. An diesem Punkt habe ich gelernt, wie ich das Wissen, das ich für REST gelernt habe, auf GraphQL anwenden kann.

Es war eine sehr interessante Erfahrung, und während der Implementierungsphase musste ich die in REST-APIs verwendeten Standardansätze und -methoden in einer GraphQL-freundlicheren Weise überdenken. In diesem Artikel versuche ich, allgemeine Probleme zusammenzufassen, die bei der erstmaligen Implementierung von GraphQL-APIs zu berücksichtigen sind.

Erforderliche Bibliotheken

GraphQL wurde intern von Facebook entwickelt und 2015 veröffentlicht. Später im Jahr 2018 wurde das GraphQL-Projekt von Facebook auf die neu gegründete GraphQL Foundation verschoben, die von der gemeinnützigen Linux Foundation gehostet wird, die die Spezifikation der GraphQL-Abfragesprache und eine Referenz pflegt und entwickelt Implementierung für JavaScript.

Da GraphQL noch eine junge Technologie ist und die erste Referenzimplementierung für JavaScript verfügbar war, existieren die ausgereiftesten Bibliotheken dafür im Node.js-Ökosystem. Es gibt auch zwei andere Unternehmen, Apollo und Prisma, die Open-Source-Tools und -Bibliotheken für GraphQL anbieten. Das Beispielprojekt in diesem Artikel basiert auf einer Referenzimplementierung von GraphQL für JavaScript und Bibliotheken, die von diesen beiden Unternehmen bereitgestellt werden:

  • Graphql-js – Eine Referenzimplementierung von GraphQL für JavaScript
  • Apollo-Server – GraphQL-Server für Express, Connect, Hapi, Koa und mehr
  • Apollo-graphql-tools – Erstellen, simulieren und heften Sie ein GraphQL-Schema mit SDL
  • Prisma-graphql-Middleware – Teilen Sie Ihre GraphQL-Resolver in Middleware-Funktionen auf

In der GraphQL-Welt beschreiben Sie Ihre APIs mit GraphQL-Schemas, und für diese definiert die Spezifikation ihre eigene Sprache namens The GraphQL Schema Definition Language (SDL). SDL ist sehr einfach und intuitiv zu bedienen und gleichzeitig extrem leistungsstark und ausdrucksstark.

Es gibt zwei Möglichkeiten, GraphQL-Schemas zu erstellen: den Code-First-Ansatz und den Schema-First-Ansatz.

  • Beim Code-First-Ansatz beschreiben Sie Ihre GraphQL-Schemas als JavaScript-Objekte basierend auf der graphql-js-Bibliothek, und die SDL wird automatisch aus dem Quellcode generiert.
  • Beim Schema-First-Ansatz beschreiben Sie Ihre GraphQL-Schemas in SDL und verbinden Ihre Geschäftslogik mit der Apollo graphql-tools-Bibliothek.

Ich persönlich bevorzuge den Schema-First-Ansatz und werde ihn für das Beispielprojekt in diesem Artikel verwenden. Wir werden ein Beispiel für einen klassischen Buchladen implementieren und ein Backend erstellen, das CRUD-APIs zum Erstellen von Autoren und Büchern sowie APIs für die Benutzerverwaltung und -authentifizierung bereitstellt.

Erstellen eines einfachen GraphQL-Servers

Um einen einfachen GraphQL-Server auszuführen, müssen wir ein neues Projekt erstellen, es mit npm initialisieren und Babel konfigurieren. Um Babel zu konfigurieren, installieren Sie zunächst die erforderlichen Bibliotheken mit dem folgenden Befehl:

 npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node

Erstellen Sie nach der Installation von Babel eine Datei mit dem Namen .babelrc im Stammverzeichnis unseres Projekts und kopieren Sie die folgende Konfiguration dorthin:

 { "presets": [ [ "@babel/env", { "targets": { "node": "current" } } ] ] }

Bearbeiten Sie auch die Datei package.json und fügen Sie den folgenden Befehl zum Abschnitt scripts hinzu:

 { ... "scripts": { "serve": "babel-node index.js" }, ... }

Nachdem wir Babel konfiguriert haben, installieren Sie die erforderlichen GraphQL-Bibliotheken mit dem folgenden Befehl:

 npm install --save express apollo-server-express graphql graphql-tools graphql-tag

Kopieren Sie nach der Installation der erforderlichen Bibliotheken dieses Code-Snippet in unsere index.js -Datei, um einen GraphQL-Server mit minimalem Setup auszuführen:

 import gql from 'graphql-tag'; import express from 'express'; import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'; const port = process.env.PORT || 8080; // Define APIs using GraphQL SDL const typeDefs = gql` type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } }; // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolvers maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });

Danach können wir unseren Server mit dem Befehl npm run serve , und wenn wir in einem Webbrowser zur URL http://localhost:8080/graphql , öffnet sich die interaktive visuelle Shell von GraphQL namens Playground, wo wir können Führen Sie GraphQL-Abfragen und -Mutationen aus und sehen Sie sich die Ergebnisdaten an.

In der GraphQL-Welt werden API-Funktionen in drei Gruppen unterteilt, die als Abfragen, Mutationen und Abonnements bezeichnet werden:

  • Abfragen werden vom Client verwendet, um die benötigten Daten vom Server anzufordern.
  • Mutationen werden vom Client verwendet, um Daten auf dem Server zu erstellen/aktualisieren/löschen.
  • Abonnements werden vom Client verwendet, um eine Echtzeitverbindung zum Server herzustellen und aufrechtzuerhalten. Dadurch kann der Client Ereignisse vom Server abrufen und entsprechend handeln.

In unserem Artikel werden wir nur Abfragen und Mutationen diskutieren. Abonnements sind ein großes Thema – sie verdienen einen eigenen Artikel und sind nicht in jeder API-Implementierung erforderlich.

Erweiterte skalare Datentypen

Sehr bald nach dem Spielen mit GraphQL werden Sie feststellen, dass SDL nur primitive Datentypen bereitstellt und erweiterte skalare Datentypen wie Date, Time und DateTime, die ein wichtiger Bestandteil jeder API sind, fehlen. Glücklicherweise haben wir eine Bibliothek, die uns hilft, dieses Problem zu lösen, und sie heißt graphql-iso-date. Nach der Installation müssen wir neue erweiterte skalare Datentypen in unserem Schema definieren und sie mit den von der Bibliothek bereitgestellten Implementierungen verbinden:

 import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date'; // Define APIs using GraphQL SDL const typeDefs = gql` scalar Date scalar Time scalar DateTime type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Date: GraphQLDate, Time: GraphQLTime, DateTime: GraphQLDateTime, Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } };

Neben Datum und Uhrzeit gibt es auch andere interessante skalare Datentypimplementierungen, die je nach Anwendungsfall für Sie nützlich sein können. Eines davon ist beispielsweise graphql-type-json, das uns die Möglichkeit gibt, dynamische Typisierung in unserem GraphQL-Schema zu verwenden und untypisierte JSON-Objekte über unsere API zu übergeben oder zurückzugeben. Es gibt auch eine Bibliothek graphql-scalar, die uns die Möglichkeit gibt, benutzerdefinierte GraphQL-Skalare mit erweiterter Bereinigung/Validierung/Transformation zu definieren.

Bei Bedarf können Sie auch Ihren benutzerdefinierten skalaren Datentyp definieren und wie oben gezeigt in Ihrem Schema verwenden. Das ist nicht schwierig, aber eine Diskussion darüber würde den Rahmen dieses Artikels sprengen – bei Interesse finden Sie weiterführende Informationen in der Apollo-Dokumentation.

Aufteilungsschema

Nachdem Sie Ihrem Schema mehr Funktionalität hinzugefügt haben, wird es wachsen und wir werden verstehen, dass es unmöglich ist, den gesamten Satz von Definitionen in einer Datei zu speichern, und wir müssen ihn in kleine Teile aufteilen, um den Code zu organisieren und ihn skalierbarer zu machen eine größere Größe. Glücklicherweise akzeptiert die von Apollo bereitgestellte Schema-Builder-Funktion makeExecutableSchema auch Schema-Definitionen und Resolver-Maps in Form eines Arrays. Dies gibt uns die Möglichkeit, unser Schema und unsere Resolver-Zuordnung in kleinere Teile aufzuteilen. Genau das habe ich in meinem Beispielprojekt getan; Ich habe die API in folgende Teile unterteilt:

  • auth.api.graphql – API zur Benutzerauthentifizierung und -registrierung
  • author.api.graphql – CRUD-API für Autoreneinträge
  • book.api.graphql – CRUD-API für Bucheinträge
  • root.api.graphql – Wurzel des Schemas und allgemeine Definitionen (wie fortgeschrittene Skalartypen)
  • user.api.graphql – CRUD-API für die Benutzerverwaltung

Während des Aufteilungsschemas gibt es eine Sache, die wir beachten müssen. Einer der Teile muss das Root-Schema sein und die anderen müssen das Root-Schema erweitern. Das klingt kompliziert, ist aber in Wirklichkeit ganz einfach. Im Stammschema werden Abfragen und Mutationen wie folgt definiert:

 type Query { ... } type Mutation { ... }

Und in den anderen sind sie wie folgt definiert:

 extend type Query { ... } extend type Mutation { ... }

Und das ist alles.

Authentifizierung und Autorisierung

Bei den meisten API-Implementierungen besteht die Anforderung, den globalen Zugriff einzuschränken und eine Art regelbasierter Zugriffsrichtlinien bereitzustellen. Dazu müssen wir in unserem Code Folgendes einführen: Authentifizierung – um die Benutzeridentität zu bestätigen – und Autorisierung – um regelbasierte Zugriffsrichtlinien durchzusetzen.

In der GraphQL-Welt, wie auch in der REST-Welt, verwenden wir im Allgemeinen zur Authentifizierung JSON Web Token. Um das übergebene JWT-Token zu validieren, müssen wir alle eingehenden Anfragen abfangen und den Autorisierungsheader darauf überprüfen. Dazu können wir während der Erstellung des Apollo-Servers eine Funktion als Kontext-Hook registrieren, der mit der aktuellen Anfrage aufgerufen wird, die den von allen Resolvern gemeinsam genutzten Kontext erstellt. Dies kann folgendermaßen erfolgen:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, context: ({ req, res }) => { const context = {}; // Verify jwt token const parts = req.headers.authorization ? req.headers.authorization.split(' ') : ['']; const token = parts.length === 2 && parts[0].toLowerCase() === 'bearer' ? parts[1] : undefined; context.authUser = token ? verify(token) : undefined; return context; } }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });

Wenn der Benutzer hier ein korrektes JWT-Token übergibt, überprüfen wir es und speichern das Benutzerobjekt im Kontext, auf den alle Resolver während der Anforderungsausführung zugreifen können.

Wir haben die Benutzeridentität überprüft, aber unsere API ist immer noch global zugänglich und nichts hindert unsere Benutzer daran, sie ohne Autorisierung aufzurufen. Eine Möglichkeit, dies zu verhindern, besteht darin, das Benutzerobjekt direkt in jedem Resolver im Kontext zu prüfen. Dies ist jedoch ein sehr fehleranfälliger Ansatz, da wir viel Boilerplate-Code schreiben müssen und wir vergessen können, die Prüfung hinzuzufügen, wenn wir einen neuen Resolver hinzufügen . Wenn wir uns die REST-API-Frameworks ansehen, werden solche Probleme im Allgemeinen mit HTTP-Anfrageabfangprogrammen gelöst, aber im Fall von GraphQL macht es keinen Sinn, da eine HTTP-Anfrage mehrere GraphQL-Abfragen enthalten kann, und wenn wir noch hinzufügen Dadurch erhalten wir nur Zugriff auf die rohe Zeichenfolgendarstellung der Abfrage und müssen sie manuell analysieren, was definitiv kein guter Ansatz ist. Dieses Konzept lässt sich nicht gut von REST auf GraphQL übertragen.

Wir brauchen also eine Möglichkeit, GraphQL-Abfragen abzufangen, und diese Methode wird Prisma-Graphql-Middleware genannt. Mit dieser Bibliothek können wir beliebigen Code ausführen, bevor oder nachdem ein Resolver aufgerufen wird. Es verbessert unsere Codestruktur, indem es die Wiederverwendung von Code und eine klare Trennung von Bedenken ermöglicht.

Die GraphQL-Community hat bereits eine Reihe großartiger Middleware auf der Grundlage der Prisma-Middleware-Bibliothek erstellt, die einige spezifische Anwendungsfälle löst, und für die Benutzerautorisierung gibt es eine Bibliothek namens graphql-shield, die uns hilft, eine Berechtigungsschicht für unsere API zu erstellen.

Nach der Installation von graphql-shield können wir eine Berechtigungsebene für unsere API wie folgt einführen:

 import { allow } from 'graphql-shield'; const isAuthorized = rule()( (obj, args, { authUser }, info) => authUser && true ); export const permissions = { Query: { '*': isAuthorized, sayHello: allow }, Mutation: { '*': isAuthorized, sayHello: allow } }

Und wir können diese Ebene wie folgt als Middleware auf unser Schema anwenden:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

Hier setzen wir beim Erstellen eines Shield-Objekts allowExternalErrors auf true, da das Verhalten des Shields standardmäßig darin besteht, Fehler abzufangen und zu behandeln, die innerhalb von Resolvern auftreten, und dies war für meine Beispielanwendung kein akzeptables Verhalten.

Im obigen Beispiel haben wir den Zugriff auf unsere API nur für authentifizierte Benutzer eingeschränkt, aber das Schild ist sehr flexibel und mit seiner Verwendung können wir ein sehr umfangreiches Autorisierungsschema für unsere Benutzer implementieren. In unserer Beispielanwendung haben wir beispielsweise zwei Rollen: USER und USER_MANAGER , und nur Benutzer mit der Rolle USER_MANAGER können die Benutzerverwaltungsfunktion aufrufen. Das wird so umgesetzt:

 export const isUserManager = rule()( (obj, args, { authUser }, info) => authUser && authUser.role === 'USER_MANAGER' ); export const permissions = { Query: { userById: isUserManager, users: isUserManager }, Mutation: { editUser: isUserManager, deleteUser: isUserManager } }

Eine weitere Sache, die ich erwähnen möchte, ist die Organisation von Middleware-Funktionen in unserem Projekt. Wie bei Schemadefinitionen und Resolver-Maps ist es besser, sie pro Schema aufzuteilen und in separaten Dateien aufzubewahren, aber im Gegensatz zum Apollo-Server, der Arrays von Schemadefinitionen und Resolver-Maps akzeptiert und sie für uns zusammenfügt, tut die Prisma-Middleware-Bibliothek dies nicht und akzeptiert nur ein Middleware-Kartenobjekt. Wenn wir sie also aufteilen, müssen wir sie manuell zusammenfügen. Um meine Lösung für dieses Problem zu sehen, sehen Sie sich bitte die ApiExplorer -Klasse im Beispielprojekt an.

Validierung

GraphQL SDL bietet eine sehr eingeschränkte Funktionalität zur Validierung von Benutzereingaben; wir können nur definieren, welches Feld erforderlich und welches optional ist. Alle weiteren Validierungsanforderungen müssen wir manuell implementieren. Wir können Validierungsregeln direkt in den Resolver-Funktionen anwenden, aber diese Funktionalität gehört wirklich nicht hierher, und dies ist ein weiterer großartiger Anwendungsfall für Benutzer-GraphQL-Middlewares. Verwenden wir beispielsweise die Eingabedaten für Benutzerregistrierungsanforderungen, bei denen wir überprüfen müssen, ob der Benutzername eine korrekte E-Mail-Adresse ist, ob die Passworteingaben übereinstimmen und das Passwort stark genug ist. Dies kann wie folgt implementiert werden:

 import { UserInputError } from 'apollo-server-express'; import passwordValidator from 'password-validator'; import { isEmail } from 'validator'; const passwordSchema = new passwordValidator() .is().min(8) .is().max(20) .has().letters() .has().digits() .has().symbols() .has().not().spaces(); export const validators = { Mutation: { signup: (resolve, parent, args, context) => { const { email, password, rePassword } = args.signupReq; if (!isEmail(email)) { throw new UserInputError('Invalid Email address!'); } if (password !== rePassword) { throw new UserInputError('Passwords don\'t match!'); } if (!passwordSchema.validate(password)) { throw new UserInputError('Password is not strong enough!'); } return resolve(parent, args, context); } } }

Und wir können die Validierungsebene als Middleware auf unser Schema anwenden, zusammen mit einer Berechtigungsebene wie dieser:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, validators, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app })

N + 1 Abfragen

Ein weiteres zu berücksichtigendes Problem, das bei GraphQL-APIs auftritt und oft übersehen wird, sind N + 1 Abfragen. Dieses Problem tritt auf, wenn wir eine Eins-zu-Viele-Beziehung zwischen Typen haben, die in unserem Schema definiert sind. Um dies beispielsweise zu demonstrieren, verwenden wir die Buch-API unseres Beispielprojekts:

 extend type Query { books: [Book!]! ... } extend type Mutation { ... } type Book { id: ID! creator: User! createdAt: DateTime! updatedAt: DateTime! authors: [Author!]! title: String! about: String language: String genre: String isbn13: String isbn10: String publisher: String publishDate: Date hardcover: Int } type User { id: ID! createdAt: DateTime! updatedAt: DateTime! fullName: String! email: String! }

Hier sehen wir, dass der User eine 1: Book Book . Die Resolver-Karte für dieses Schema ist wie folgt definiert:

 export const resolvers = { Query: { books: (obj, args, context, info) => { return bookService.findAll(); }, ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { return userService.findById(creatorId); }, ... } }

Wenn wir mit dieser API eine Buchabfrage ausführen und uns das Protokoll der SQL-Anweisungen ansehen, sehen wir etwa Folgendes:

 select `books`.* from `books` select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? ...

Es ist leicht zu erraten – während der Ausführung wurde der Resolver zuerst für die Bücherabfrage aufgerufen, die eine Liste von Büchern zurückgab, und dann wurde jedes Buchobjekt als Erstellerfeld-Resolver bezeichnet, und dieses Verhalten verursachte N + 1 Datenbankabfragen. Wenn wir unsere Datenbank nicht explodieren lassen wollen, ist ein solches Verhalten nicht wirklich toll.

Um das N + 1-Abfrageproblem zu lösen, haben die Facebook-Entwickler eine sehr interessante Lösung namens DataLoader entwickelt, die auf ihrer README-Seite wie folgt beschrieben wird:

„DataLoader ist ein generisches Dienstprogramm, das als Teil der Datenabrufebene Ihrer Anwendung verwendet werden kann, um eine vereinfachte und konsistente API über verschiedene Remote-Datenquellen wie Datenbanken oder Webdienste über Batching und Caching bereitzustellen.“

Es ist nicht sehr einfach zu verstehen, wie DataLoader funktioniert, also sehen wir uns zuerst das Beispiel an, das das oben gezeigte Problem löst, und erklären dann die Logik dahinter.

In unserem Beispielprojekt ist DataLoader für das Erstellerfeld wie folgt definiert:

 export class UserDataLoader extends DataLoader { constructor() { const batchLoader = userIds => { return userService .findByIds(userIds) .then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) ); }; super(batchLoader); } static getInstance(context) { if (!context.userDataLoader) { context.userDataLoader = new UserDataLoader(); } return context.userDataLoader; } }

Sobald wir UserDataLoader definiert haben, können wir den Resolver des Erstellerfelds wie folgt ändern:

 export const resolvers = { Query: { ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { const userDataLoader = UserDataLoader.getInstance(context); return userDataLoader.load(creatorId); }, ... } }

Wenn wir nach den angewendeten Änderungen die Buchabfrage erneut ausführen und uns das Protokoll der SQL-Anweisungen ansehen, sehen wir etwa Folgendes:

 select `books`.* from `books` select `users`.* from `users` where `id` in (?)

Hier können wir sehen, dass N + 1 Datenbankabfragen auf zwei Abfragen reduziert wurden, wobei die erste die Liste der Bücher auswählt und die zweite die Liste der Benutzer auswählt, die als Urheber in der Liste der Bücher präsentiert werden. Lassen Sie uns nun erklären, wie DataLoader dieses Ergebnis erzielt.

Die Hauptfunktion von DataLoader ist Batching. Während der einzelnen Ausführungsphase sammelt DataLoader alle unterschiedlichen IDs aller einzelnen Ladefunktionsaufrufe und ruft dann die Stapelfunktion mit allen angeforderten IDs auf. Eine wichtige Sache, an die Sie sich erinnern sollten, ist, dass die Instanzen von DataLoaders nicht wiederverwendet werden können. Sobald die Stapelfunktion aufgerufen wird, werden die zurückgegebenen Werte für immer in der Instanz zwischengespeichert. Aufgrund dieses Verhaltens müssen wir für jede Ausführungsphase eine neue Instanz von DataLoader erstellen. Um dies zu erreichen, haben wir eine statische getInstance -Funktion erstellt, die prüft, ob die Instanz von DataLoader in einem Kontextobjekt präsentiert wird, und, wenn sie nicht gefunden wird, eines erstellt. Denken Sie daran, dass für jede Ausführungsphase ein neues Kontextobjekt erstellt und von allen Resolvern gemeinsam genutzt wird.

Eine Stapelladefunktion von DataLoader akzeptiert ein Array unterschiedlicher angeforderter IDs und gibt ein Versprechen zurück, das in ein Array entsprechender Objekte aufgelöst wird. Beim Schreiben einer Stapelladefunktion müssen wir zwei wichtige Dinge beachten:

  1. Das Array der Ergebnisse muss die gleiche Länge wie das Array der angeforderten IDs haben. Wenn wir beispielsweise die IDs [1, 2, 3] angefordert haben, muss das zurückgegebene Ergebnisarray genau drei Objekte enthalten: [{ "id": 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }, { “id”: 3, “fullName”: “user3” }]
  2. Jeder Index im Ergebnisarray muss demselben Index im Array der angeforderten IDs entsprechen. Wenn das Array der angeforderten IDs beispielsweise die folgende Reihenfolge hat: [3, 1, 2] , muss das zurückgegebene Array der Ergebnisse Objekte in genau derselben Reihenfolge enthalten: [{ "id": 3, “fullName”: “user3” }, { “id”: 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }]

In unserem Beispiel stellen wir mit dem folgenden Code sicher, dass die Reihenfolge der Ergebnisse mit der Reihenfolge der angeforderten IDs übereinstimmt:

 then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) )

Sicherheit

Und nicht zuletzt möchte ich die Sicherheit erwähnen. Mit GraphQL können wir sehr flexible APIs erstellen und dem Benutzer umfangreiche Möglichkeiten zum Abfragen der Daten bieten. Dies verleiht der Client-Seite der Anwendung ziemlich viel Macht und, wie Uncle Ben sagte: „Mit großer Macht kommt große Verantwortung.“ Ohne angemessene Sicherheit kann ein böswilliger Benutzer eine teure Anfrage senden und einen DoS-Angriff (Denial of Service) auf unseren Server verursachen.

Das erste, was wir tun können, um unsere API zu schützen, besteht darin, die Selbstprüfung des GraphQL-Schemas zu deaktivieren. Standardmäßig stellt ein GraphQL-API-Server die Fähigkeit bereit, sein gesamtes Schema zu überprüfen, was im Allgemeinen von interaktiven visuellen Shells wie GraphiQL und Apollo Playground verwendet wird, aber es kann auch für einen böswilligen Benutzer sehr nützlich sein, eine komplexe Abfrage auf der Grundlage unserer API zu erstellen . Wir können dies deaktivieren, indem wir beim Erstellen des Apollo-Servers den introspection Parameter auf false setzen:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

Als Nächstes können wir unsere API schützen, indem wir die Tiefe der Abfrage begrenzen. Dies ist besonders wichtig, wenn wir eine zyklische Beziehung zwischen unseren Datentypen haben. In unserem Beispiel hat beispielsweise der Projekttyp „ Author “ das Feld „Bücher“ und der Typ „ Book “ das Feld „Autoren“. Dies ist eindeutig eine zyklische Beziehung, und nichts hindert böswillige Benutzer daran, eine Abfrage wie diese zu schreiben:

 query { authors { id, fullName books { id, title authors { id, fullName books { id, title, authors { id, fullName books { id, title authors { ... } } } } } } } }

Es ist klar, dass eine solche Abfrage bei ausreichender Verschachtelung unseren Server leicht explodieren lassen kann. Um die Tiefe der Abfragen zu begrenzen, können wir eine Bibliothek namens graphql-depth-limit verwenden. Nach der Installation können wir beim Erstellen von Apollo Server eine Tiefenbeschränkung anwenden, wie folgt:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false, validationRules: [ depthLimit(5) ] }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

Hier haben wir die maximale Tiefe der Abfragen auf fünf begrenzt.

Post Scriptum: Der Wechsel von REST zu GraphQL ist interessant

In diesem Tutorial habe ich versucht, allgemeine Probleme aufzuzeigen, auf die Sie stoßen werden, wenn Sie mit der Implementierung von GraphQL-APIs beginnen. Einige Teile davon bieten jedoch sehr flache Codebeispiele und kratzen aufgrund ihrer Größe nur an der Oberfläche des besprochenen Problems. Aus diesem Grund finden Sie vollständigere Codebeispiele im Git-Repository meines GraphQL-API-Beispielprojekts: graphql-example.

Abschließend möchte ich sagen, dass GraphQL eine wirklich interessante Technologie ist. Wird es REST ersetzen? Niemand weiß, vielleicht wird es morgen in der sich schnell verändernden Welt der IT einen besseren Ansatz zur Entwicklung von APIs geben, aber GraphQL fällt wirklich in die Kategorie der interessanten Technologien, die es definitiv wert sind, erlernt zu werden.