Kapitel 4. Ein Schema entwerfen

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

GraphQL wird deinen Entwurfsprozess verändern. Anstatt deine APIs als eine Sammlung von REST-Endpunkten zu betrachten, wirst du deine APIs als Sammlungen von Typen betrachten. Bevor du mit deiner neuen API loslegst, musst du über die Datentypen, die deine API offenlegen wird, nachdenken, sie besprechen und formell definieren. Diese Sammlung von Typen wird als Schema bezeichnet.

Schema First ist eine Entwurfsmethodik, die alle Teams auf die gleiche Seite der Datentypen bringt, aus denen deine Anwendung besteht. Das Backend-Team hat eine klare Vorstellung von den Daten, die es speichern und bereitstellen muss. Das Frontend-Team hat die Definitionen, die es braucht, um mit der Entwicklung von Benutzeroberflächen zu beginnen. Alle haben ein klares Vokabular, mit dem sie über das System, das sie aufbauen, kommunizieren können. Kurz gesagt: Alle können sich an die Arbeit machen.

Um die Definition von Typen zu vereinfachen, verfügt GraphQL über eine Sprache, mit der wir unsere Schemata definieren können: die Schema Definition Language (SDL). Genau wie die GraphQL Query Language ist die GraphQL SDL unabhängig von der Sprache oder dem Framework, das du für deine Anwendungen verwendest, gleich. GraphQL-Schemadokumente sind Textdokumente, die die in einer Anwendung verfügbaren Typen definieren und später sowohl von Clients als auch von Servern zur Validierung von GraphQL-Anfragen verwendet werden.

In diesem Kapitel werfen wir einen Blick auf die GraphQL SDL und erstellen ein Schema für eine Foto-Sharing-Anwendung.

Typen definieren

Der beste Weg, um etwas über GraphQL-Typen und Schemata zu lernen, ist, eines zu bauen. In der Anwendung zum Teilen von Fotos können sich die Nutzer mit ihrem GitHub-Konto anmelden, um Fotos zu posten und Nutzer auf diesen Fotos zu markieren. Die Verwaltung von Nutzern und Beiträgen ist eine Funktion, die für fast jede Internetanwendung wichtig ist.

Die PhotoShare-Anwendung wird zwei Haupttypen haben: User und Photo. Beginnen wir mit dem Entwurf des Schemas für die gesamte Anwendung.

Typen

Die zentrale Einheit eines jeden GraphQL-Schemas ist der Typ. In GraphQL stellt ein Typein benutzerdefiniertes Objekt dar und diese Objekte beschreiben die Kernfunktionen deiner Anwendung. Eine Social-Media-Anwendung besteht zum Beispiel ausUsers und Posts. Ein Blog würde aus Categories und Articles bestehen. Die Typen repräsentieren die Daten deiner Anwendung.

Wenn du Twitter von Grund auf neu aufbauen würdest, würde ein Post den Text enthalten, den der Nutzer senden möchte. (In diesem Fall wäre Tweet vielleicht ein besserer Name für diesen Typ.) Wenn du Snapchat aufbaust, würde Postein Bild enthalten, das besserSnap genannt werden sollte. Wenn du ein Schema definierst, legst du eine gemeinsame Sprache fest, die dein Team verwenden wird, wenn es über deine Domänenobjekte spricht.

Ein Typ hat Felder, die die mit jedem Objekt verbundenen Daten darstellen. Jedes Feld gibt eine bestimmte Art von Daten zurück. Das kann eine ganze Zahl oder ein String sein, aber auch ein benutzerdefinierter Objekttyp oder eine Liste von Typen.

Ein Schema ist eine Sammlung von Typdefinitionen. Du kannst deine Schemas in eine JavaScript-Datei als String oder in eine beliebige Textdatei schreiben. Diese Dateien haben normalerweise die Erweiterung .graphql.

Definieren wir den ersten GraphQL-Objekttyp in unserer Schemadatei - Photo:

type Photo {
    id: ID!
    name: String!
    url: String!
    description: String
}

Zwischen den geschweiften Klammern haben wir die Felder von Photodefiniert. Die url der Photoist ein Verweis auf den Speicherort der Bilddatei. Diese Beschreibung enthält auch einige Metadaten über das Photo: ein name und ein description. Schließlich hat jedes Photo ein ID, einen eindeutigen Bezeichner, der als Schlüssel für den Zugriff auf das Foto verwendet werden kann.

Jedes Feld enthält Daten eines bestimmten Typs. Wir haben in unserem Schema nur einen benutzerdefinierten Typ definiert, den Photo, aber GraphQL verfügt über einige eingebaute Typen, die wir für unsere Felder verwenden können. Diese eingebauten Typen werden skalare Typen genannt. Die Felder description, name und url verwenden den Skalar-TypString. Die Daten, die zurückgegeben werden, wenn wir diese Felder abfragen, sind JSON-Strings. Das Ausrufezeichen gibt an, dass das Feld nicht nullbar ist, was bedeutet, dass die Felder name und url bei jeder Abfrage Daten zurückgeben müssen. description ist löschbar, was bedeutet, dass die Fotobeschreibungen optional sind. Bei einer Abfrage könnte dieses Feld null zurückgeben.

Das Feld Photo's ID gibt einen eindeutigen Bezeichner für jedes Foto an. In GraphQL wird der Skalar-Typ ID verwendet, wenn ein eindeutiger Bezeichner zurückgegeben werden soll. Der JSON-Wert für diesen Bezeichner ist ein String, der jedoch als eindeutiger Wert validiert wird.

Skalare Typen

Die eingebauten Skalartypen von GraphQL (Int, Float, String, Boolean,ID) sind sehr nützlich, aber es kann vorkommen, dass du deine eigenen Skalartypen definieren möchtest. Ein Skalartyp ist kein Objekttyp. Er hat keine Felder. Bei der Implementierung eines GraphQL-Dienstes kannst du jedoch angeben, wie benutzerdefinierte Skalartypen validiert werden sollen, zum Beispiel:

scalar DateTime

type Photo {
    id: ID!
    name: String!
    url: String!
    description: String
    created: DateTime!
}

Hier haben wir einen benutzerdefinierten Skalar-Typ erstellt: DateTime. Jetzt können wir herausfinden, wann jedes Foto created war. Jedes Feld, das mit DateTime gekennzeichnet ist, gibt einen JSON-String zurück, aber wir können den benutzerdefinierten Skalar verwenden, um sicherzustellen, dass dieser String serialisiert, validiert und als offizielles Datum und Uhrzeit formatiert werden kann.

Du kannst benutzerdefinierte Skalare für jeden Typ deklarieren, den du validieren musst.

Hinweis

Das graphql-custom-types npm-Paket enthält einige häufig verwendete benutzerdefinierte Skalartypen, die du schnell zu deinem Node.js GraphQL-Dienst hinzufügen kannst.

Enums

Aufzählungstypen oder Enums sind skalare Typen, die es einem Feld ermöglichen, einen begrenzten Satz von Stringwerten zurückzugeben. Wenn du sicherstellen willst, dass ein Feld einen Wert aus einer begrenzten Menge von Werten zurückgibt, kannst du einen enum Typ verwenden.

Erstellen wir zum Beispiel einen enum Typ mit dem Namen PhotoCategory, der die Art des Fotos definiert, das gepostet wird, und zwar aus einer Reihe von fünf möglichen Optionen: SELFIE, PORTRAIT, ACTION, LANDSCAPE oder GRAPHIC:

enum PhotoCategory {
    SELFIE
    PORTRAIT
    ACTION
    LANDSCAPE
    GRAPHIC
}

Du kannst Aufzählungstypen verwenden, um Felder zu definieren. Fügen wir ein categoryFeld zu unserem Photo Objekttyp hinzu:

type Photo {
    id: ID!
    name: String!
    url: String!
    description: String
    created: DateTime!
    category: PhotoCategory!
}

Jetzt, wo wir category hinzugefügt haben, stellen wir sicher, dass er einen der fünf gültigen Werte zurückgibt, wenn wir den Dienst implementieren.

Hinweis

Es spielt keine Rolle, ob deine Implementierung volle Unterstützung für Aufzählungstypen hat. Du kannst GraphQL-Aufzählungsfelder in jeder Sprache implementieren.

Verbindungen und Listen

Wenn du GraphQL-Schemas erstellst, kannst du Felder definieren, die Listen von beliebigen GraphQL-Typen zurückgeben. Listen werden erstellt, indem man einen GraphQL-Typ mit eckigen Klammern umgibt. [String] definiert eine Liste von Strings und[PhotoCategory] eine Liste von Fotokategorien. Wie in Kapitel 3 beschrieben, können Listen auch aus mehreren Typen bestehen, wenn wir die Typenunion oder interface einbeziehen. Am Ende dieses Kapitels werden wir diese Arten von Listen ausführlicher besprechen.

Manchmal kann das Ausrufezeichen bei der Definition von Listen ein wenig knifflig sein. Wenn das Ausrufezeichen nach der schließenden eckigen Klammer steht, bedeutet es, dass das Feld selbst nicht nullbar ist. Wenn das Ausrufezeichen vor der schließenden eckigen Klammer steht, bedeutet das, dass die in der Liste enthaltenen Werte nicht nullbar sind. Wo immer du ein Ausrufezeichen siehst, ist der Wert erforderlich und kann nicht null zurückgeben. Tabelle 4-1 definiert diese verschiedenen Situationen.

Tabelle 4-1. Nullbarkeitsregeln mit Listen
Liste Erklärung Definition

[Int]

Eine Liste von löschbaren Integer-Werten

[Int!]

Eine Liste von nicht-nullbaren Integer-Werten

[Int]!

Eine nicht löschbare Liste von löschbaren Integer-Werten

[Int!]!

Eine nicht-nullbare Liste von nicht-nullbaren Integer-Werten

Die meisten Listendefinitionen sind nicht-nullbare Listen mit nicht-nullbaren Werten. Das liegt daran, dass wir in der Regel nicht wollen, dass die Werte in unserer Liste Null sind. Wir sollten alle Nullwerte im Voraus herausfiltern. Wenn unsere Liste keine Werte enthält, können wir einfach ein leeres JSON-Array zurückgeben, zum Beispiel[]. Ein leeres Array ist technisch gesehen nicht null: Es ist einfach ein Array, das keine Werte enthält.

Die Möglichkeit, Daten zu verbinden und mehrere Arten von zusammenhängenden Daten abzufragen, ist eine sehr wichtige Funktion. Wenn wir Listen mit unseren benutzerdefinierten Objekttypen erstellen, nutzen wir diese leistungsstarke Funktion und verbinden Objekte miteinander.

In diesem Abschnitt erfährst du, wie du eine Liste verwenden kannst, um Objekttypen zu verbinden.

Eins-zu-Eins-Verbindungen

Wenn wir Felder erstellen, die auf benutzerdefinierten Objekttypen basieren, verbinden wir zwei Objekte. In der Graphentheorie wird eine Verbindung oder ein Link zwischen zwei Objekten als Kante bezeichnet. Die erste Art von Verbindung ist eine Eins-zu-Eins-Verbindung, bei der wir einen einzelnen Objekttyp mit einem anderen einzelnen Objekttyp verbinden.

Fotos werden von Nutzern gepostet, daher sollte jedes Foto in unserem System eine Kante enthalten, die das Foto mit dem Nutzer verbindet, der es gepostet hat. Abbildung 4-1 zeigt eine einseitige Verbindung zwischen zwei Typen: Photo und User. Die Kante, die die beiden Knoten miteinander verbindet, heißt postedBy.

One-To-One Connection
Abbildung 4-1. Eins-zu-eins-Verbindung

Schauen wir uns an, wie wir dies im Schema definieren würden:

type User {
    githubLogin: ID!
    name: String
    avatar: String
}

type Photo {
    id: ID!
    name: String!
    url: String!
    description: String
    created: DateTime!
    category: PhotoCategory!
    postedBy: User!
}

Zunächst haben wir einen neuen Typ zu unserem Schema hinzugefügt, User. Die Nutzer der PhotoShare-Anwendung werden sich über GitHub anmelden. Wenn der Nutzer sich anmeldet, erhalten wir seine githubLogin und verwenden sie als eindeutigen Bezeichner für seinen Nutzerdatensatz. Wenn er/sie seinen/ihren Namen oder sein/ihr Foto auf GitHub hinzugefügt hat, speichern wir diese Informationen optional in den Feldernname und avatar.

Als Nächstes fügen wir die Verbindung hinzu, indem wir ein postedBy Feld zum Fotoobjekt hinzufügen. Da jedes Foto von einem Nutzer gepostet werden muss, wird dieses Feld auf den Typ User! gesetzt; das Ausrufezeichen wird hinzugefügt, damit das Feld nicht leer ist.

One-to-Many-Verbindungen

Es ist eine gute Idee, GraphQL-Dienste möglichst ungerichtet zu halten. Das gibt unseren Kunden die größtmögliche Flexibilität bei der Erstellung von Abfragen, da sie den Graphen von jedem beliebigen Knoten aus durchlaufen können. Alles, was wir tun müssen, um diese Praxis zu befolgen, ist, einen Pfad zurück von UserTypen zu Photo Typen zu erstellen. Das bedeutet, dass wir bei einer Abfrage von User alle Fotos sehen sollten, die ein bestimmter Nutzer gepostet hat:

type User {
    githubLogin: ID!
    name: String
    avatar: String
    postedPhotos: [Photo!]!
}

Indem wir das Feld postedPhotos zum Typ User hinzugefügt haben, haben wir einen Pfad zurück zum Photo des Nutzers erstellt. Das Feld postedPhotos gibt eine Liste der Photo Typen zurück, also der Fotos, die vom übergeordneten Benutzer gepostet wurden. Da ein Benutzer viele Fotos posten kann, haben wir eine One-to-many-Verbindung erstellt. One-to-many-Verbindungen, wie sie in Abbildung 4-2 dargestellt sind, werden häufig erstellt, wenn ein übergeordnetes Objekt ein Feld enthält, das andere Objekte auflistet.

One-To-Many Connection
Abbildung 4-2. One-to-many-Verbindung

Ein gängiger Ort, um One-to-Many-Verbindungen hinzuzufügen, sind unsere Root-Typen. Um unsere Fotos oder Benutzer in einer Abfrage verfügbar zu machen, müssen wir die Felder unseres Query Stammtyps definieren. Schauen wir uns an, wie wir unsere neuen benutzerdefinierten Typen zum Root-Typ Query hinzufügen können:

type Query {
    totalPhotos: Int!
    allPhotos: [Photo!]!
    totalUsers: Int!
    allUsers: [User!]!
}

schema {
    query: Query
}

Das Hinzufügen des Typs Query definiert die Abfragen, die in unserer API verfügbar sind. In diesem Beispiel haben wir zwei Abfragen für jeden Typ hinzugefügt: eine, um die Gesamtzahl der verfügbaren Datensätze für jeden Typ zu liefern, und eine weitere, um die vollständige Liste dieser Datensätze zu liefern. Außerdem haben wir den Typ Query auf schema als Datei hinzugefügt. Damit sind unsere Abfragen in unserer GraphQL-API verfügbar.

Jetzt können unsere Fotos und Benutzer mit dem folgenden Abfrage-String abgefragt werden:

query {
    totalPhotos
    allPhotos {
        name
        url
    }
}

Many-to-Many-Verbindungen

Manchmal wollen wir Listen von Knotenpunkten mit anderen Listen von Knotenpunkten verbinden. Unsere PhotoShare-Anwendung ermöglicht es Nutzern, andere Nutzer auf jedem Foto, das sie veröffentlichen, zu identifizieren. Dieser Vorgang wird als Tagging bezeichnet. Ein Foto kann aus vielen Nutzern bestehen, und ein Nutzer kann in vielen Fotos markiert sein, wie Abbildung 4-3 zeigt.

Many to Many Connection
Abbildung 4-3. Many-to-many-Verbindung

Um diese Art von Verbindung zu erstellen, müssen wir Listenfelder sowohl zu den Typen User als auch zu Photo hinzufügen.

type User {
    ...
    inPhotos: [Photo!]!
}

type Photo {
    ...
    taggedUsers: [User!]!
}

Wie du siehst, besteht eine Many-to-Many-Verbindung aus zweiOne-to-Many-Verbindungen. In diesem Fall kann eine Photo viele getaggte Nutzer haben und eine User kann in vielen Fotos getaggt sein.

Durch Typen

Manchmal möchtest du bei der Erstellung von Many-to-Many-Beziehungen einige Informationen über die Beziehung selbst speichern. Da es in unserer Foto-Sharing-App keinen wirklichen Bedarf für einen Durchgangstyp gibt, verwenden wir ein anderes Beispiel, um einen Durchgangstyp zu definieren: eine Freundschaft zwischen Benutzern.

Wir können viele Nutzer mit vielen Nutzern verbinden, indem wir ein Feld unterUser definieren, das eine Liste anderer Nutzer enthält:

type User {
    friends: [User!]!
}

Hier haben wir für jeden Nutzer eine Liste von Freunden definiert. Nehmen wir an, wir wollen Informationen über die Freundschaft selbst speichern, z. B. wie lange sich die Nutzer kennen oder wo sie sich kennengelernt haben.

In diesem Fall müssen wir die Kante als einen benutzerdefinierten Objekttyp definieren. Wir nennen dieses Objekt einen Durchgangstyp, weil es ein Knoten ist, der zwei Knoten verbinden soll. Definieren wir einen Durchgangstyp namensFriendship, mit dem wir zwei Freunde verbinden können, der aber auch Daten darüber liefert, wie die Freunde verbunden sind:

type User {
    friends: [Friendship!]!
}
type Friendship {
    friend_a: User!
    friend_b: User!
    howLong: Int!
    whereWeMet: Location
}

Anstatt das Feld friends direkt auf einer Liste von anderenUser Typen zu definieren, haben wir ein Friendship erstellt, um die friends zu verbinden. Der TypFriendship definiert die beiden verbundenen Freunde: friend_a undfriend_b. Er definiert auch einige Detailfelder darüber, wie die Freunde verbunden sind: howLong und whereWeMet. Das Feld howLong ist ein Int, das die Länge der Freundschaft definiert, und das Feld whereWeMet verweist auf einen benutzerdefinierten Typ namens Location.

Wir können das Design des Friendship Typs verbessern, indem wir eine Gruppe von Freunden als Teil der Freundschaft zulassen. Vielleicht hast du deine besten Freunde in der ersten Klasse zur gleichen Zeit kennengelernt. Wir können zwei oder mehr Freunde in die Freundschaft aufnehmen, indem wir ein einziges Feld namens friends hinzufügen:

    type Friendship {
        friends: [User!]!
        how_long: Int!
        where_we_met: Location
    }

Wir haben nur ein Feld für alle friends in einerFriendship aufgenommen. Jetzt kann dieser Typ zwei oder mehr Freunde widerspiegeln.

Listen mit verschiedenen Typen

In GraphQL müssen unsere Listen nicht immer den gleichen Typ zurückgeben. In Kapitel 3 haben wir die Typen union und interfaces vorgestellt und gelernt, wie man mit Fragmenten Abfragen für diese Typen schreibt. Schauen wir uns nun an, wie wir diese Typen in unser Schema einfügen können.

Wir werden hier einen Zeitplan als Beispiel verwenden. Du könntest einen Zeitplan haben, der aus verschiedenen Ereignissen besteht, die jeweils unterschiedliche Datenfelder erfordern. Zum Beispiel können die Details zu einem Treffen einer Lerngruppe oder einem Training völlig unterschiedlich sein, aber du solltest in der Lage sein, beide zu einem Zeitplan hinzuzufügen. Du kannst dir einen Tagesplan als eine Liste mit verschiedenen Arten von Aktivitäten vorstellen.

Es gibt zwei Möglichkeiten, wie wir ein Schema für einen Zeitplan in GraphQL definieren können: Unions und Schnittstellen.

Gewerkschaftstypen

In GraphQL ist ein Union-Typ ein Typ, mit dem wir einen von mehreren verschiedenen Typen zurückgeben können. In Kapitel 3 haben wir eine Abfrage namens schedule geschrieben, die eine Agenda abfragte und andere Daten zurückgab, wenn es sich bei dem Tagesordnungspunkt um ein Training handelte, als wenn es sich um eine Lerngruppe handelte. Schauen wir uns das hier noch einmal an:

query schedule {
    agenda {
        ...on Workout {
            name
            reps
        }
        ...on StudyGroup {
            name
            subject
            students
        }
    }
}

In der täglichen Agenda des Schülers/der Schülerin könnten wir dies durch die Erstellung eines unionTyps namens AgendaItem handhaben:

union AgendaItem = StudyGroup | Workout

type StudyGroup {
    name: String!
    subject: String
    students: [User!]!
}

type Workout {
    name: String!
    reps: Int!
}

type Query {
    agenda: [AgendaItem!]!
}

AgendaItem fasst Lerngruppen und Trainings unter einem einzigen Typ zusammen. Wenn wir das Feld agenda zu unserem Query hinzufügen, definieren wir es als eine Liste von Trainings oder Lerngruppen.

Es ist möglich, so viele Typen wie gewünscht unter einer einzigen Vereinigung zusammenzufassen. Trenne einfach jeden Typ mit einer Pipe:

union = StudyGroup | Workout | Class | Meal | Meeting | FreeTime

Schnittstellen

Eine andere Möglichkeit, Felder zu behandeln, die mehrere Typen enthalten können, ist die Verwendung einer Schnittstelle. Schnittstellen sind abstrakte Typen, die von Objekttypen implementiert werden können. Eine Schnittstelle definiert alle Felder, die in jedem Objekt enthalten sein müssen, das sie implementiert. Schnittstellen sind eine gute Möglichkeit, den Code in deinem Schema zu organisieren. So wird sichergestellt, dass bestimmte Typen immer bestimmte Felder enthalten, die abgefragt werden können, egal welcher Typ zurückgegeben wird.

In Kapitel 3 haben wir eine Abfrage für ein agenda geschrieben, die eine Schnittstelle verwendet, um Felder für verschiedene Elemente in einem Zeitplan zurückzugeben. Das wollen wir hier wiederholen:

query schedule {
  agenda {
    name
    start
    end
    ...on Workout {
      reps
    }
  }
}

So könnte die Abfrage einer agenda aussehen, die Schnittstellen implementiert. Damit ein Typ eine Schnittstelle zu unserem Zeitplan bilden kann, muss er bestimmte Felder enthalten, die alle Tagesordnungspunkte implementieren. Zu diesen Feldern gehören die Zeiten name, start und end. Es spielt keine Rolle, welche Art von Terminplanungselement du hast, sie alle brauchen diese Angaben, um in einem Terminplan aufgeführt zu werden.

Hier sehen wir, wie wir diese Lösung in unserem GraphQL-Schema umsetzen würden:

scalar DataTime

interface AgendaItem {
    name: String!
    start: DateTime!
    end: DateTime!
}

type StudyGroup implements AgendaItem {
    name: String!
    start: DateTime!
    end: DateTime!
    participants: [User!]!
    topic: String!
}

type Workout implements AgendaItem {
    name: String!
    start: DateTime!
    end: DateTime!
    reps: Int!
}

type Query {
    agenda: [AgendaItem!]!
}

In diesem Beispiel erstellen wir eine Schnittstelle namens AgendaItem. Diese Schnittstelle ist ein abstrakter Typ, den andere Typen implementieren können. Wenn ein anderer Typ eine Schnittstelle implementiert, muss er die von der Schnittstelle definierten Felder enthalten. Sowohl StudyGroup als auch Workout implementieren die Schnittstelle AgendaItem, also müssen beide die Felder name, start und endverwenden. Die Abfrage agenda gibt eine Liste der AgendaItem Typen zurück. Jeder Typ, der die Schnittstelle AgendaItem implementiert, kann in der Listeagenda zurückgegeben werden.

Beachte auch, dass diese Typen auch andere Felder implementieren können. EinStudyGroup hat ein topic und eine Liste von participants, und ein Workouthat noch reps. Du kannst diese zusätzlichen Felder in einer Abfrage auswählen, indem du Fragmente verwendest.

Sowohl Union-Typen als auch Schnittstellen sind Werkzeuge, mit denen du Felder erstellen kannst, die verschiedene Objekttypen enthalten. Es liegt an dir zu entscheiden, wann du das eine oder das andere verwendest. Im Allgemeinen ist es eine gute Idee, Union-Typen zu verwenden, wenn die Objekte völlig unterschiedliche Felder enthalten. Sie sind sehr effektiv. Wenn ein Objekttyp bestimmte Felder enthalten muss, um eine Schnittstelle zu einem anderen Objekttyp zu bilden, musst du eher eine Schnittstelle als einen Union-Typ verwenden.

Argumente

Argumente können zu jedem Feld in GraphQL hinzugefügt werden. Sie ermöglichen es uns, Daten zu senden, die das Ergebnis unserer GraphQL-Operationen beeinflussen können. In Kapitel 3 haben wir uns mit Benutzerargumenten in unseren Abfragen und Mutationen beschäftigt. Schauen wir uns nun an, wie wir Argumente in unserem Schema definieren.

Der Typ Query enthält Felder, die allUsers oderallPhotos auflisten, aber was passiert, wenn du nur einen User oder einen Photo auswählen möchtest? Du müsstest einige Informationen über den Benutzer oder das Foto angeben, das du auswählen möchtest. Du kannst diese Informationen zusammen mit meiner Anfrage als Argument senden:

type Query {
    ...
    User(githubLogin: ID!): User!
    Photo(id: ID!): Photo!
}

Genau wie ein Feld muss auch ein Argument einen Typ haben. Dieser Typ kann mit einem der skalaren Typen oder Objekttypen definiert werden, die in unserem Schema verfügbar sind. Um einen bestimmten Benutzer auszuwählen, müssen wir seine eindeutige githubLogin als Argument senden. Die folgende Abfrage wählt nur den Namen und den Avatar von MoonTahoeaus:

query {
    User(githubLogin: "MoonTahoe") {
        name
        avatar
    }
}

Um Details über ein einzelnes Foto auszuwählen, müssen wir die ID des Fotos angeben:

query {
    Photo(id: "14TH5B6NS4KIG3H4S") {
        name
        description
        url
    }
}

In beiden Fällen waren Argumente erforderlich, um Details über einen bestimmten Datensatz abzufragen. Da diese Argumente erforderlich sind, werden sie als nicht-nullbare Felder definiert. Wenn wir die id odergithubLogin bei diesen Abfragen nicht angeben, gibt der GraphQL-Parser einen Fehler zurück.

Daten filtern

Die Argumente müssen nicht nullbar sein. Wir können optionale Argumente mit nullbaren Feldern hinzufügen. Das bedeutet, dass wir Argumente als optionale Parameter angeben können, wenn wir Abfrageoperationen ausführen. Wir könnten zum Beispiel die Fotoliste, die von der Abfrage allPhotos zurückgegeben wird, nach Fotokategorien filtern:

type Query {
    ...
    allPhotos(category: PhotoCategory): [Photo!]!
}

Wir haben der Abfrage allPhotos ein optionales Feld category hinzugefügt. Die Kategorie muss mit den Werten des AufzählungstypsPhotoCategory übereinstimmen. Wenn kein Wert mit der Abfrage gesendet wird, können wir davon ausgehen, dass dieses Feld jedes Foto zurückgibt. Wenn jedoch eine Kategorie angegeben wird, sollten wir eine gefilterte Liste von Fotos in derselben Kategorie erhalten:

query {
    allPhotos(category: "SELFIE") {
        name
        description
        url
    }
}

Diese Abfrage würde die name, description und url von jedem Foto zurückgeben, das als SELFIE kategorisiert wurde.

Datenauslagerung

Wenn unsere PhotoShare-Anwendung erfolgreich ist, was sie sein wird, wird sie eine Menge Users und Photos haben. Es ist vielleicht nicht möglich, jede User oder jedePhoto in unserer Anwendung zurückzugeben. Wir können GraphQL-Argumente verwenden, um die Menge der Daten zu steuern, die von unseren Abfragen zurückgegeben werden. Dieser Prozess wird Data Paging genannt, weil eine bestimmte Anzahl von Datensätzen zurückgegeben wird, um eine Seite von Daten darzustellen.

Um das Paging von Daten zu implementieren, fügen wir zwei optionale Argumente hinzu:first, um die Anzahl der Datensätze zu erfassen, die in einer einzigen Datenseite zurückgegeben werden sollen, und start, um die Startposition oder den Index des ersten zurückzugebenden Datensatzes zu definieren. Wir können diese Argumente zu unseren beiden Listenabfragen hinzufügen:

type Query {
    ...
    allUsers(first: Int=50 start: Int=0): [User!]!
    allPhotos(first: Int=25 start: Int=0): [Photo!]!
}

Im vorangegangenen Beispiel haben wir optionale Argumente für first und start hinzugefügt. Wenn der Kunde diese Argumente nicht mit der Abfrage angibt, verwenden wir die angegebenen Standardwerte. Die Abfrage allUsers liefert standardmäßig nur die ersten 50 Nutzer und die Abfrage allPhotos liefert nur die ersten 25 Fotos.

Der Kunde kann einen anderen Bereich von Benutzern oder Fotos abfragen, indem er Werte für diese Argumente angibt. Wenn wir zum Beispiel die Benutzer 90 bis 100 auswählen wollen, können wir dies mit der folgenden Abfrage tun:

query {
    allUsers(first: 10 start: 90) {
        name
        avatar
    }
}

Diese Abfrage wählt nur 10 Jahre aus, beginnend mit dem 90sten Nutzer. Sie sollte die name und avatar für diesen speziellen Bereich von Nutzern zurückgeben. Wir können die Gesamtzahl der Seiten, die auf dem Client verfügbar sind, berechnen, indem wir die Gesamtzahl der Einträge durch die Größe einer Datenseite teilen:

pages = pageSize/total

Sortieren

Wenn wir eine Liste von Daten abfragen, möchten wir vielleicht auch festlegen, wie die zurückgegebene Liste sortiert werden soll. Auch dafür können wir Argumente verwenden.

Stellen wir uns ein Szenario vor, in dem wir die Möglichkeit einbauen wollen, beliebige Listen von Photo Datensätzen zu sortieren. Eine Möglichkeit, diese Herausforderung zu meistern, besteht darin, enums zu erstellen, die angeben, welche Felder zum Sortieren von PhotoObjekten verwendet werden können, sowie Anweisungen, wie diese Felder zu sortieren sind:

enum SortDirection {
    ASCENDING
    DESCENDING
}

enum SortablePhotoField {
    name
    description
    category
    created
}

Query {
    allPhotos(
        sort: SortDirection = DESCENDING
        sortBy: SortablePhotoField = created
    ): [Photo!]!
}

Hier haben wir die Argumente sort und sortBy zu der Abfrage allPhotoshinzugefügt. Wir haben einen Aufzählungstyp namens SortDirection erstellt, mit dem wir die Werte des Arguments sort auf ASCENDING oderDESCENDING beschränken können. Wir haben auch einen weiteren Aufzählungstyp fürSortablePhotoField erstellt. Da wir die Fotos nicht nach irgendeinem Feld sortieren wollen, haben wir die Werte von sortBy auf vier der Fotofelder beschränkt: name description , category oder created (das Datum und die Uhrzeit, zu der das Foto hinzugefügt wurde). Sowohl sort als auch sortBy sind optionale Argumente, d.h. sie werden standardmäßig auf DESCENDING und created gesetzt, wenn keines der Argumente angegeben wird.

Kunden können jetzt selbst bestimmen, wie ihre Fotos sortiert werden, wenn sie eineallPhotos Abfrage stellen:

query {
    allPhotos(sortBy: name)
}

Diese Abfrage gibt alle Fotos sortiert nach absteigendem Namen zurück.

Bisher haben wir Argumente nur zu Feldern des Typs Query hinzugefügt, aber es ist wichtig zu wissen, dass du Argumente zu jedem Feld hinzufügen kannst. Wir könnten die Argumente zum Filtern, Sortieren und Blättern zu den Fotos hinzufügen, die von einem einzelnen Nutzer gepostet wurden:

type User {
    postedPhotos(
        first: Int = 25
        start: Int = 0
        sort: SortDirection = DESCENDING
        sortBy: SortablePhotoField = created
        category: PhotoCategory
    ): [Photo!]

Das Hinzufügen von Paginierungsfiltern kann dazu beitragen, die Menge der Daten zu reduzieren, die eine Abfrage zurückgeben kann. Wir besprechen die Idee der Datenbegrenzung ausführlicher in Kapitel 7.

Mutationen

Mutationen müssen im Schema definiert werden. Genau wie Abfragen werden auch Mutationen in einem eigenen benutzerdefinierten Objekttyp definiert und dem Schema hinzugefügt. Technisch gesehen gibt es keinen Unterschied zwischen der Definition einer Mutation oder einer Abfrage in deinem Schema. Der Unterschied liegt in der Absicht. Wir sollten Mutationen nur dann erstellen, wenn eine Aktion oder ein Ereignis etwas am Zustand unserer Anwendung ändert.

Mutationen sollten die Verben in deiner Anwendung darstellen. Sie sollten aus den Dingen bestehen, die die Nutzer mit deinem Dienst tun können sollen. Wenn du deinen GraphQL-Dienst entwirfst, solltest du eine Liste mit allen Aktionen erstellen, die ein Nutzer mit deiner Anwendung durchführen kann. Das sind höchstwahrscheinlich deine Mutationen.

In der PhotoShare App können sich Nutzer/innen bei GitHub anmelden, Fotos posten und Fotos markieren. Alle diese Aktionen ändern etwas am Status der Anwendung. Nachdem sie sich bei GitHub angemeldet haben, ändern sich die aktuellen Nutzer, die auf den Client zugreifen. Wenn ein Nutzer ein Foto einstellt, gibt es ein zusätzliches Foto im System. Das Gleiche gilt für das Taggen von Fotos. Jedes Mal, wenn ein Foto markiert wird, werden neue Foto-Tag-Datensätze erstellt.

Wir können diese Mutationen zu unserem Stammmutationstyp in unserem Schema hinzufügen und sie für den Kunden verfügbar machen. Beginnen wir mit unserer ersten Mutation,postPhoto:

type Mutation {
    postPhoto(
        name: String!
        description: String
        category: PhotoCategory=PORTRAIT
    ): Photo!
}

schema {
    query: Query
    mutation: Mutation
}

Durch das Hinzufügen eines Feldes unter dem Typ Mutation mit dem Namen postPhoto können die Nutzer/innen Fotos einstellen. Zumindest können die Nutzer damit Metadaten zu den Fotos posten. Das Hochladen der eigentlichen Fotos wird in Kapitel 7 behandelt.

Wenn ein Nutzer ein Foto postet, ist mindestens die Angabe name erforderlich. Die Angaben description und category sind optional. Wenn das Argument category nicht angegeben wird, wird das gepostete Foto standardmäßig auf PORTRAIT gesetzt. Ein Benutzer kann zum Beispiel ein Foto posten, indem er die folgende Mutation sendet:

mutation {
    postPhoto(name: "Sending the Palisades") {
        id
        url
        created
        postedBy {
            name
        }
    }
}

Nachdem der/die Nutzer/in ein Foto gepostet hat, kann er/sie Informationen über das Foto auswählen, das er/sie gerade gepostet hat. Das ist gut, denn einige der Datensatzdetails über das neue Foto werden auf dem Server erstellt. Die IDfür unser neues Foto wird von der Datenbank erstellt. Die urldes Fotos wird automatisch erstellt. Außerdem wird das Foto mit einem Zeitstempel versehen, der das Datum und die Uhrzeit angibt, zu der das Foto created wurde. Diese Abfrage wählt alle diese neuen Felder aus, nachdem ein Foto gepostet worden ist.

Außerdem enthält die Auswahlmenge Informationen über den Nutzer, der das Foto gepostet hat. Ein Benutzer muss angemeldet sein, um ein Foto zu posten. Wenn kein Benutzer angemeldet ist, sollte diese Mutation einen Fehler zurückgeben. Wenn ein Benutzer angemeldet ist, können wir über das Feld postedBy herausfinden, wer das Foto gepostet hat. In Kapitel 5 wird beschrieben, wie man einen autorisierten Benutzer mithilfe eines Zugangstokens authentifiziert.

Eingabe-Typen

Wie du vielleicht bemerkt hast, sind die Argumente für einige unserer Abfragen und Mutationen ziemlich lang geworden. Es gibt eine bessere Möglichkeit, diese Argumente zu organisieren, indem du Eingabetypen verwendest. Ein Eingabetyp ist ähnlich wie ein GraphQL-Objekttyp, nur dass er nur für Eingabeargumente verwendet wird.

Lass uns die postPhoto Mutation verbessern, indem wir einen Eingabetyp für unsere Argumente verwenden:

input PostPhotoInput {
  name: String!
  description: String
  category: PhotoCategory=PORTRAIT
}

type Mutation {
    postPhoto(input: PostPhotoInput!): Photo!
}

Der Typ PostPhotoInput ist wie ein Objekttyp, aber er wurde nur für Eingabeargumente geschaffen. Er erfordert ein name und dasdescription, aber die category Felder sind weiterhin optional. Wenn du jetzt die postPhoto Mutation sendest, müssen die Details über das neue Foto in einem Objekt enthalten sein:

mutation newPhoto($input: PostPhotoInput!) {
    postPhoto(input: $input) {
        id
        url
        created
    }
}

Wenn wir diese Mutation erstellen, setzen wir den Typ der Abfragevariablen $input so, dass er unserem Eingabetyp PostPhotoInput! entspricht. Sie ist nicht nullbar, weil wir mindestens auf das Feld input.name zugreifen müssen, um ein neues Foto hinzuzufügen. Wenn wir die Mutation senden, müssen wir die neuen Fotodaten in unsere Abfragevariablen eingeben, die unter dem Feld input verschachtelt sind:

{
    "input": {
        "name": "Hanging at the Arc",
        "description": "Sunny on the deck of the Arc",
        "category": "LANDSCAPE"
    }
}

Unser Input wird in einem JSON-Objekt zusammengefasst und zusammen mit der Mutation in den Abfragevariablen unter dem Schlüssel "input" gesendet. Da die Abfragevariablen als JSON formatiert sind, muss die Kategorie eine Zeichenkette sein, die zu einer der Kategorien aus dem Typ PhotoCategory passt.

Eingabetypen sind der Schlüssel zum Organisieren und Schreiben eines klaren GraphQL-Schemas. Du kannst Eingabetypen als Argumente für jedes Feld verwenden. Mit ihnen kannst du sowohl das Paging als auch das Filtern von Daten in Anwendungen verbessern.

Schauen wir uns an, wie wir alle unsere Sortier- und Filterfelder mit Hilfe von Eingabetypen organisieren und wiederverwenden können:

input PhotoFilter {
    category: PhotoCategory
    createdBetween: DateRange
    taggedUsers: [ID!]
    searchText: String
}

input DateRange {
    start: DateTime!
    end: DateTime!
}

input DataPage {
    first: Int = 25
    start: Int = 0
}

input DataSort {
    sort: SortDirection = DESCENDING
    sortBy: SortablePhotoField = created
}

type User {
    ...
    postedPhotos(filter:PhotoFilter paging:DataPage sorting:DataSort): [Photo!]!
    inPhotos(filter:PhotoFilter paging:DataPage sorting:DataSort): [Photo!]!
}

type Photo {
    ...
    taggedUsers(sorting:DataSort): [User!]!
}

type Query {
    ...
    allUsers(paging:DataPage sorting:DataSort): [User!]!
    allPhotos(filter:PhotoFilter paging:DataPage sorting:DataSort): [Photo!]!
}

Wir haben zahlreiche Felder unter Eingabetypen organisiert und diese Felder als Argumente in unserem Schema wiederverwendet.

Die Eingabetypen PhotoFilter enthalten optionale Eingabefelder, mit denen der Kunde eine Liste von Fotos filtern kann. Der Typ PhotoFilter enthält einen verschachtelten Eingabetyp, DateRange, unter dem Feld createdBetween.DateRange muss Start- und Enddatum enthalten. MitPhotoFilter können wir Fotos auch nach category, searchstring oder taggedUsers filtern. Wir fügen all diese Filteroptionen zu jedem Feld hinzu, das eine Liste von Fotos liefert. So kann der Kunde selbst bestimmen, welche Fotos in der Liste angezeigt werden.

Es wurden auch Eingabetypen für das Blättern und Sortieren erstellt. Der EingabetypDataPage enthält die Felder, die benötigt werden, um eine Seite mit Daten anzufordern, und der Eingabetyp DataSort enthält unsere Sortierfelder. Diese Eingabetypen wurden zu jedem Feld in unserem Schema hinzugefügt, das eine Liste von Daten liefert.

Wir können eine Abfrage schreiben, die ziemlich komplexe Eingabedaten akzeptiert, indem wir die verfügbaren Eingabetypen verwenden:

query getPhotos($filter:PhotoFilter $page:DataPage $sort:DataSort) {
    allPhotos(filter:$filter paging:$page sorting:$sort) {
        id
        name
        url
    }
}

Diese Abfrage akzeptiert optional Argumente für drei Eingabetypen:$filter, $page und $sort. Mit Hilfe von Abfragevariablen können wir einige spezifische Details über die Fotos, die wir zurückgeben möchten, senden:

{
    "filter": {
        "category": "ACTION",
        "taggedUsers": ["MoonTahoe", "EvePorcello"],
        "createdBetween": {
            "start": "2018-11-6",
            "end": "2018-5-31"
        }
    },
    "page": {
        "first": 100
    }
}

Diese Abfrage findet alle ACTION Fotos, für die die GitHub-BenutzerMoonTahoe und EvePorcello zwischen dem 6. November und dem 31. Mai, der Skisaison, getaggt sind. Wir fragen auch nach den ersten 100 Fotos mit dieser Abfrage.

Eingabetypen helfen uns, unser Schema zu organisieren und Argumente wiederzuverwenden. Sie verbessern auch die Schema-Dokumentation, die GraphiQL oder GraphQL Playground automatisch erstellen. Dadurch wird deine API zugänglicher und leichter zu erlernen und zu verdauen. Schließlich kannst du Eingabetypen verwenden, um dem Client eine Menge Macht zu geben, um organisierte Abfragen auszuführen.

Rückgabe-Typen

Alle Felder in unserem Schema haben unsere HaupttypenUser und Photo zurückgegeben. Aber manchmal müssen wir zusätzlich zu den eigentlichen Nutzdaten auch Metainformationen über Abfragen und Mutationen zurückgeben. Wenn sich zum Beispiel ein Nutzer angemeldet hat und authentifiziert wurde, müssen wir zusätzlich zu den UserNutzdaten ein Token zurückgeben.

Um sich mit GitHub OAuth anzumelden, müssen wir einen OAuth-Code von GitHub erhalten. Wir besprechen die Einrichtung deines eigenen GitHub OAuth-Kontos und die Beschaffung des GitHub-Codes in "GitHub-Autorisierung". Gehen wir zunächst davon aus, dass du einen gültigen GitHub-Code hast, den du an die githubAuth Mutation senden kannst, um einen Nutzer anzumelden:

type AuthPayload {
    user: User!
    token: String!
}

type Mutation {
    ...
    githubAuth(code: String!): AuthPayload!
}

Die Benutzer werden authentifiziert, indem sie einen gültigen GitHub-Code an diegithubAuth Mutation senden. Bei Erfolg wird ein benutzerdefinierter Objekttyp zurückgegeben, der sowohl Informationen über den erfolgreich angemeldeten Benutzer als auch ein Token enthält, das zur Autorisierung weiterer Abfragen und Mutationen, einschließlich der postPhoto Mutation, verwendet werden kann.

Du kannst benutzerdefinierte Rückgabetypen für jedes Feld verwenden, für das wir mehr als einfache Nutzdaten benötigen. Vielleicht wollen wir wissen, wie lange es dauert, bis eine Abfrage eine Antwort liefert, oder wie viele Ergebnisse in einer bestimmten Antwort gefunden wurden, zusätzlich zu den Nutzdaten der Abfrage. All dies kannst du mit einem benutzerdefinierten Rückgabetyp erledigen.

An dieser Stelle haben wir alle Typen vorgestellt, die dir bei der Erstellung von GraphQL-Schemas zur Verfügung stehen. Wir haben uns sogar ein wenig Zeit genommen, um Techniken zu besprechen, die dir helfen können, dein Schema besser zu gestalten. Aber es gibt noch einen letzten Stammobjekttyp, den wir vorstellen müssen - den TypSubscription.

Abonnements

Subscription Typen unterscheiden sich nicht von anderen Objekttypen in der GraphQL-Schemadefinitionssprache. Hier definieren wir die verfügbaren Abonnements als Felder in einem benutzerdefinierten Objekttyp. Wir müssen sicherstellen, dass die Abonnements das PubSub-Entwurfsmuster zusammen mit einer Art Echtzeittransport implementieren, wenn wir später in Kapitel 7 den GraphQL-Dienst aufbauen.

Wir können zum Beispiel Abonnements hinzufügen, die es unseren Kunden ermöglichen, auf die Erstellung neuer Photo oder User Typen zu warten:

type Subscription {
    newPhoto: Photo!
    newUser: User!
}

schema {
    query: Query
    mutation: Mutation
    subscription: Subscription
}

Hier erstellen wir ein benutzerdefiniertes Subscription Objekt, das zwei Felder enthält:newPhoto und newUser. Wenn ein neues Foto gepostet wird, wird dieses neue Foto an alle Kunden gesendet, die dasnewPhoto Abonnement abonniert haben. Wenn ein neuer Benutzer erstellt wurde, werden seine Daten an alle Kunden weitergeleitet, die nach neuen Benutzern suchen.

Genau wie Abfragen oder Mutationen können auch Abonnements Argumente nutzen. Nehmen wir an, wir möchten dem newPhotoAbonnement Filter hinzufügen, die bewirken, dass es nur nach neuen ACTION Fotos sucht:

type Subscription {
    newPhoto(category: PhotoCategory): Photo!
    newUser: User!
}

Wenn Nutzer das newPhoto Abonnement abonnieren, haben sie jetzt die Möglichkeit, die Fotos zu filtern, die an dieses Abonnement gesendet werden. Um zum Beispiel nur neue Fotos von ACTION zu filtern, können Kunden die folgende Operation an unsere GraphQL-API senden:

subscription {
    newPhoto(category: "ACTION") {
        id
        name
        url
        postedBy {
            name
        }
    }
}

Dieses Abonnement sollte nur Details für ACTION Fotos liefern.

Eine Subskription ist eine großartige Lösung, wenn es wichtig ist, Daten in Echtzeit zu verarbeiten. In Kapitel 7 erfahren wir mehr über die Implementierung von Subskriptionen für alle deine Anforderungen bei der Verarbeitung von Echtzeitdaten.

Schema Dokumentation

In Kapitel 3 wird erklärt, wie GraphQL über ein Introspektionssystem verfügt, mit dem du herausfinden kannst, welche Abfragen der Server unterstützt. Wenn du ein GraphQL-Schema schreibst, kannst du optionale Beschreibungen für jedes Feld hinzufügen, die zusätzliche Informationen über die Typen und Felder des Schemas liefern. Die Bereitstellung von Beschreibungen kann es deinem Team, dir selbst und anderen Nutzern der API erleichtern, dein Typsystem zu verstehen.

Fügen wir zum Beispiel Kommentare zum Typ User in unserem Schema hinzu:

"""
A user who has been authorized by GitHub at least once
"""
type User {

    """
    The user's unique GitHub login
    """
    githubLogin: ID!

    """
    The user's first and last name
    """
    name: String

    """
    A url for the user's GitHub profile photo
    """
    avatar: String

    """
    All of the photos posted by this user
    """
    postedPhotos: [Photo!]!

    """
    All of the photos in which this user appears
    """
    inPhotos: [Photo!]!

}

Indem du drei Anführungszeichen über und unter deinem Kommentar zu jedem Typ oder Feld hinzufügst, gibst du den Benutzern ein Wörterbuch für deine API an die Hand. Zusätzlich zu den Typen und Feldern kannst du auch Argumente dokumentieren. Schauen wir uns die Mutation postPhotoan:

Replace with:

  type Mutation {
    """
    Authorizes a GitHub User
    """
    githubAuth(
      "The unique code from GitHub that is sent to authorize the user"
      code: String!
    ): AuthPayload!
  }

Die Argument-Kommentare teilen den Namen des Arguments und ob das Feld optional ist. Wenn du Eingabetypen verwendest, kannst du diese wie jeden anderen Typ dokumentieren:

"""
The inputs sent with the postPhoto Mutation
"""
input PostPhotoInput {
  "The name of the new photo"
  name: String!
  "(optional) A brief description of the photo"
  description: String
  "(optional) The category that defines the photo"
  category: PhotoCategory=PORTRAIT
}

postPhoto(
      "input: The name, description, and category for a new photo"
      input: PostPhotoInput!
): Photo!

Alle diese Dokumentationshinweise werden dann in der Schemadokumentation im GraphQL Playground oder GraphiQL aufgeführt, wie in Abbildung 4-4 gezeigt. Natürlich kannst du auch Introspektionsanfragen stellen, um die Beschreibungen dieser Typen zu finden.

postPhoto Documentation
Abbildung 4-4. postPhoto Dokumentation

Das Herzstück aller GraphQL-Projekte ist ein solides, gut definiertes Schema.Dieses dient als Fahrplan und Vertrag zwischen den Frontend- und Backend-Teams, um sicherzustellen, dass das erstellte Produkt immer dem Schema entspricht.

In diesem Kapitel haben wir ein Schema für unsere Foto-Sharing-Anwendung erstellt. In den nächsten drei Kapiteln zeigen wir dir, wie du eine vollwertige GraphQL-Anwendung erstellst, die den Vertrag des gerade erstellten Schemas erfüllt.

Get GraphQL lernen now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.