Einstieg in GraphQL mit AWS AppSync

Avatar of Nader Dabit
Nader Dabit am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

GraphQL wird immer beliebter. Das Problem ist, dass Sie als Frontend-Entwickler nur die halbe Miete haben. GraphQL ist nicht nur eine Client-Technologie. Der Server muss ebenfalls gemäß der Spezifikation implementiert werden. Das bedeutet, dass Sie, um GraphQL in Ihre Anwendung zu implementieren, nicht nur GraphQL auf der Frontend-Seite lernen müssen, sondern auch Best Practices für GraphQL, serverseitige Entwicklung und alles, was dazugehört, auf der Backend-Seite.

Es wird eine Zeit kommen, in der Sie sich auch mit Problemen wie der Skalierung Ihres Servers, komplexen Autorisierungsszenarien, bösartigen Abfragen und weiteren Problemen befassen müssen, die mehr Fachwissen und ein noch tieferes Verständnis dessen erfordern, was traditionell als Backend-Entwicklung kategorisiert wird.

Glücklicherweise gibt es heute eine Reihe von Managed-Backend-Dienstanbietern, die es Frontend-Entwicklern ermöglichen, sich nur auf die Implementierung von Features im Frontend zu konzentrieren, ohne sich mit all der traditionellen Backend-Arbeit auseinandersetzen zu müssen.

Dienste wie Firebase (API) / AWS AppSync (Datenbank), Cloudinary (Medien), Algolia (Suche) und Auth0 (Authentifizierung) ermöglichen es uns, unsere komplexe Infrastruktur an einen Drittanbieter auszulagern und uns stattdessen darauf zu konzentrieren, den Endbenutzern in Form neuer Features Mehrwert zu bieten.

In diesem Tutorial lernen wir, wie wir AWS AppSync, einen Managed GraphQL-Dienst, nutzen können, um eine Full-Stack-Anwendung zu erstellen, ohne eine einzige Zeile Backend-Code schreiben zu müssen.

Während das Framework, mit dem wir arbeiten, React ist, sind die Konzepte und API-Aufrufe, die wir verwenden werden, Framework-unabhängig und funktionieren genauso in Angular, Vue, React Native, Ionic oder jedem anderen JavaScript-Framework oder jeder anderen JavaScript-Anwendung.

Wir werden eine Restaurant-Bewertungs-App erstellen. In dieser App werden wir in der Lage sein, ein Restaurant zu erstellen, Restaurants anzuzeigen, eine Bewertung für ein Restaurant zu erstellen und Bewertungen für ein Restaurant anzuzeigen.

An image showing four different screenshots of the restaurant app in mobile view.

Die Tools und Frameworks, die wir verwenden werden, sind React, AWS Amplify und AWS AppSync.

AWS Amplify ist ein Framework, das es uns ermöglicht, Cloud-Dienste wie Authentifizierung, GraphQL-APIs und Lambda-Funktionen zu erstellen und damit zu verbinden, unter anderem. AWS AppSync ist ein Managed GraphQL-Dienst.

Wir werden Amplify verwenden, um eine AppSync-API zu erstellen und uns damit zu verbinden, und dann den clientseitigen React-Code schreiben, um mit der API zu interagieren.

Repo ansehen

Erste Schritte

Als Erstes erstellen wir ein React-Projekt und wechseln in das neue Verzeichnis

npx create-react-app ReactRestaurants

cd ReactRestaurants

Als Nächstes installieren wir die Abhängigkeiten, die wir für dieses Projekt verwenden werden. AWS Amplify ist die JavaScript-Bibliothek, die wir für die Verbindung mit der API verwenden werden, und wir verwenden Glamor für das Styling.

yarn add aws-amplify glamor

Als Nächstes müssen wir die Amplify CLI installieren und konfigurieren

npm install -g @aws-amplify/cli

amplify configure

Amplify configure führt Sie durch die Schritte, die erforderlich sind, um mit der Erstellung von AWS-Diensten in Ihrem Konto zu beginnen. Eine Anleitung dazu finden Sie in diesem Video.

Nachdem die App erstellt wurde und Amplify einsatzbereit ist, können wir ein neues Amplify-Projekt initialisieren.

amplify init

Amplify init führt Sie durch die Schritte zur Initialisierung eines neuen Amplify-Projekts. Es wird nach Ihrem gewünschten Projektnamen, Umgebungsnamen und Texteditor gefragt. Die CLI erkennt automatisch Ihre React-Umgebung und wählt intelligente Standardeinstellungen für die restlichen Optionen.

Erstellung der GraphQL-API

Nachdem wir ein neues Amplify-Projekt initialisiert haben, können wir nun die Restaurant-Bewertungs-GraphQL-API hinzufügen. Um einen neuen Dienst hinzuzufügen, können wir den Befehl amplify add ausführen.

amplify add api

Dies führt uns durch die folgenden Schritte, um die API einzurichten

? Please select from one of the below mentioned services GraphQL
? Provide API name bigeats
? Choose an authorization type for the API API key
? Do you have an annotated GraphQL schema? N
? Do you want a guided schema creation? Y
? What best describes your project: Single object with fields
? Do you want to edit the schema now? Y

Die CLI sollte nun ein grundlegendes Schema im Texteditor öffnen. Dies wird das Schema für unsere GraphQL-API sein.

Fügen Sie das folgende Schema ein und speichern Sie es.

// amplify/backend/api/bigeats/schema.graphql

type Restaurant @model {
  id: ID!
  city: String!
  name: String!
  numRatings: Int
  photo: String!
  reviews: [Review] @connection(name: "RestaurantReview")
}
type Review @model {
  rating: Int!
  text: String!
  createdAt: String
  restaurant: Restaurant! @connection(name: "RestaurantReview")
}

In diesem Schema erstellen wir zwei Haupttypen: Restaurant und Review. Beachten Sie, dass wir @model und @connection Direktiven in unserem Schema haben.

Diese Direktiven sind Teil des GraphQL Transform Tools, das in die Amplify CLI integriert ist. GraphQL Transform nimmt ein Basis-Schema, das mit Direktiven dekoriert ist, und transformiert unseren Code in eine voll funktionsfähige API, die das Basis-Datenmodell implementiert.

Wenn wir unsere eigene GraphQL-API aufbauen würden, müssten wir all dies manuell erledigen

  1. Definieren Sie das Schema
  2. Definieren Sie die Operationen gegen das Schema (Queries, Mutations und Subscriptions)
  3. Erstellen Sie die Datenquellen
  4. Schreiben Sie Resolver, die zwischen den Schema-Operationen und den Datenquellen zuordnen.

Mit der @model Direktive generiert das GraphQL Transform Tool alle Schema-Operationen, Resolver und Datenquellen, sodass wir nur das Basis-Schema definieren müssen (Schritt 1). Die @connection Direktive ermöglicht es uns, Beziehungen zwischen den Modellen zu modellieren und die entsprechenden Resolver für die Beziehungen zu generieren.

In unserem Schema verwenden wir @connection, um eine Beziehung zwischen Restaurant und Reviews zu definieren. Dies erstellt eine eindeutige Kennung für die Restaurant-ID für die Bewertung im endgültig generierten Schema.

Nachdem wir unser Basis-Schema erstellt haben, können wir die API in unserem Konto erstellen.

amplify push
? Are you sure you want to continue? Yes
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes

Da wir eine GraphQL-Anwendung erstellen, müssten wir normalerweise alle unsere lokalen GraphQL-Abfragen, Mutationen und Abonnements von Grund auf neu schreiben. Stattdessen wird die CLI unser GraphQL-Schema analysieren und dann alle Definitionen für uns generieren und lokal für uns speichern, damit wir sie verwenden können.

Nachdem dies abgeschlossen ist, wurde das Backend erstellt und wir können von unserer React-Anwendung aus darauf zugreifen.

Wenn Sie Ihre AppSync-API im AWS-Dashboard anzeigen möchten, besuchen Sie https://console.aws.amazon.com/appsync und klicken Sie auf Ihre API. Über das Dashboard können Sie das Schema, die Datenquellen und die Resolver anzeigen. Sie können auch Abfragen und Mutationen mit dem integrierten GraphQL-Editor durchführen.

Erstellung des React-Clients

Jetzt, da die API erstellt ist und wir Daten aus unserer API abfragen und erstellen können. Wir werden drei Operationen verwenden, um mit unserer API zu interagieren

  1. Erstellung eines neuen Restaurants
  2. Abfrage von Restaurants und ihren Bewertungen
  3. Erstellung einer Bewertung für ein Restaurant

Bevor wir mit dem Erstellen der App beginnen, werfen wir einen Blick darauf, wie diese Operationen aussehen und funktionieren werden.

Interaktion mit der AppSync GraphQL API

Bei der Arbeit mit einer GraphQL-API gibt es viele verfügbare GraphQL-Clients.

Wir können jeden beliebigen GraphQL-Client verwenden, mit dem wir mit einer AppSync-GraphQL-API interagieren möchten, aber es gibt zwei, die speziell für die einfachste Verwendung konfiguriert sind. Dies sind der Amplify-Client (den wir verwenden werden) und das AWS AppSync JS SDK (ähnliche API wie der Apollo-Client).

Der Amplify-Client ähnelt der Fetch-API insofern, als er Promise-basiert und leicht verständlich ist. Der Amplify-Client unterstützt Offline-Funktionen nicht sofort. Das AppSync SDK ist komplexer, unterstützt aber Offline-Funktionen sofort.

Um die AppSync-API mit Amplify aufzurufen, verwenden wir die API-Kategorie. Hier ist ein Beispiel für den Aufruf einer Query

import { API, graphqlOperation } from 'aws-amplify'
import * as queries from './graphql/queries'

const data = await API.graphql(graphqlOperation(queries.listRestaurants))

Für eine Mutation ist es sehr ähnlich. Der einzige Unterschied ist, dass wir ein zweites Argument für die Daten übergeben müssen, die wir in der Mutation senden

import { API, graphqlOperation } from 'aws-amplify'
import * as mutations from './graphql/mutations'

const restaurant = { name: "Babalu", city: "Jackson" }
const data = await API.graphql(graphqlOperation(
  mutations.createRestaurant,
  { input: restaurant }
))

Wir verwenden die Methode graphql aus der API-Kategorie, um den Vorgang aufzurufen, und umschließen ihn mit graphqlOperation, das GraphQL-Abfragezeichenfolgen in die Standard-GraphQL-AST parst.

Wir werden diese API-Kategorie für alle unsere GraphQL-Vorgänge in der App verwenden.

Hier ist das Repo mit dem endgültigen Code für dieses Projekt.

Konfiguration der React-App mit Amplify

Als Erstes müssen wir in unserer App konfigurieren, dass sie unsere Amplify-Anmeldeinformationen erkennt. Als wir unsere API erstellt haben, hat die CLI eine neue Datei namens aws-exports.js in unserem src-Ordner erstellt.

Diese Datei wird von der CLI erstellt und aktualisiert, während wir Dienste erstellen, aktualisieren und löschen. Diese Datei werden wir verwenden, um die React-Anwendung über unsere Dienste zu informieren.

Um die App zu konfigurieren, öffnen Sie src/index.js und fügen Sie den folgenden Code hinzu

import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

Als Nächstes erstellen wir die Dateien, die wir für unsere Komponenten benötigen werden. Erstellen Sie im Verzeichnis src die folgenden Dateien

  • Header.js
  • Restaurant.js
  • Review.js
  • CreateRestaurant.js
  • CreateReview.js

Erstellung der Komponenten

Obwohl die Stile in den folgenden Code-Schnipseln referenziert werden, wurden die Stil-Definitionen weggelassen, um die Schnipsel kürzer zu halten. Stil-Definitionen finden Sie im finalen Projekt-Repo.

Als Nächstes erstellen wir die Header-Komponente, indem wir src/Header.js aktualisieren.

// src/Header.js

import React from 'react'
import { css } from 'glamor'
const Header = ({ showCreateRestaurant }) => (
  <div {...css(styles.header)}>
    <p {...css(styles.title)}>BigEats</p>
    <div {...css(styles.iconContainer)}>
      <p {...css(styles.icon)} onClick={showCreateRestaurant}>+</p>
    </div>
  </div>
)

export default Header

Nachdem unsere Header-Komponente erstellt wurde, aktualisieren wir src/App.js. Diese Datei enthält alle Interaktionen mit der API, daher ist sie ziemlich groß. Wir werden die Methoden definieren und sie als Props an die Komponenten weitergeben, die sie aufrufen werden.

// src/App.js

import React, { Component } from 'react'
import { API, graphqlOperation } from 'aws-amplify'

import Header from './Header'
import Restaurants from './Restaurants'
import CreateRestaurant from './CreateRestaurant'
import CreateReview from './CreateReview'
import Reviews from './Reviews'
import * as queries from './graphql/queries'
import * as mutations from './graphql/mutations'

class App extends Component {
  state = {
    restaurants: [],
    selectedRestaurant: {},
    showCreateRestaurant: false,
    showCreateReview: false,
    showReviews: false
  }
  async componentDidMount() {
    try {
      const rdata = await API.graphql(graphqlOperation(queries.listRestaurants))
      const { data: { listRestaurants: { items }}} = rdata
      this.setState({ restaurants: items })
    } catch(err) {
      console.log('error: ', err)
    }
  }
  viewReviews = (r) => {
    this.setState({ showReviews: true, selectedRestaurant: r })
  }
  createRestaurant = async(restaurant) => {
    this.setState({
      restaurants: [...this.state.restaurants, restaurant]
    })
    try {
      await API.graphql(graphqlOperation(
        mutations.createRestaurant,
        {input: restaurant}
      ))
    } catch(err) {
      console.log('error creating restaurant: ', err)
    }
  }
  createReview = async(id, input) => {
    const restaurants = this.state.restaurants
    const index = restaurants.findIndex(r => r.id === id)
    restaurants[index].reviews.items.push(input)
    this.setState({ restaurants })
    await API.graphql(graphqlOperation(mutations.createReview, {input}))
  }
  closeModal = () => {
    this.setState({
      showCreateRestaurant: false,
      showCreateReview: false,
      showReviews: false,
      selectedRestaurant: {}
    })
  }
  showCreateRestaurant = () => {
    this.setState({ showCreateRestaurant: true })
  }
  showCreateReview = r => {
    this.setState({ selectedRestaurant: r, showCreateReview: true })
  }
  render() {
    return (
      <div>
        <Header showCreateRestaurant={this.showCreateRestaurant} />
        <Restaurants
          restaurants={this.state.restaurants}
          showCreateReview={this.showCreateReview}
          viewReviews={this.viewReviews}
        />
        {
          this.state.showCreateRestaurant && (
            <CreateRestaurant
              createRestaurant={this.createRestaurant}
              closeModal={this.closeModal}   
            />
          )
        }
        {
          this.state.showCreateReview && (
            <CreateReview
              createReview={this.createReview}
              closeModal={this.closeModal}   
              restaurant={this.state.selectedRestaurant}
            />
          )
        }
        {
          this.state.showReviews && (
            <Reviews
              selectedRestaurant={this.state.selectedRestaurant}
              closeModal={this.closeModal}
              restaurant={this.state.selectedRestaurant}
            />
          )
        }
      </div>
    );
  }
}
export default App

Wir erstellen zunächst einen anfänglichen Zustand, um das Restaurant-Array zu speichern, das wir aus unserer API abrufen werden. Wir erstellen auch Booleans, um unsere UI zu steuern, und ein selectedRestaurant-Objekt.

In componentDidMount fragen wir die Restaurants ab und aktualisieren den Zustand, um die von der API abgerufenen Restaurants zu speichern.

In createRestaurant und createReview senden wir Mutationen an die API. Beachten Sie auch, dass wir ein optimistisches Update bereitstellen, indem wir den Zustand sofort aktualisieren, damit die UI vor dem Eintreffen der Antwort aktualisiert wird, um unsere UI reaktionsschnell zu gestalten.

Als Nächstes erstellen wir die Restaurants-Komponente (src/Restaurants.js).

// src/Restaurants.js

import React, { Component } from 'react';
import { css } from 'glamor'

class Restaurants extends Component {
  render() {
    const { restaurants, viewReviews } = this.props
    return (
      <div {...css(styles.container)}>
        {
          restaurants.length === Number(0) && (
            <h1
              {...css(styles.h1)}
            >Create your first restaurant by clicking +</h1>
          )
        }
        {
          restaurants.map((r, i) => (
            <div key={i}>
              <img
                src={r.photo}
                {...css(styles.image)}
              />
              <p {...css(styles.title)}>{r.name}</p>
              <p {...css(styles.subtitle)}>{r.city}</p>
              <p
                onClick={() => viewReviews(r)}
                {...css(styles.viewReviews)}
              >View Reviews</p>
              <p
                onClick={() => this.props.showCreateReview(r)}
                {...css(styles.createReview)}
              >Create Review</p>
            </div>
          ))
        }
      </div>
    );
  }
}

export default Restaurants

Diese Komponente ist die Hauptansicht der App. Wir bilden die Liste der Restaurants ab und zeigen das Bild des Restaurants, seinen Namen und seinen Standort sowie Links an, die Overlays öffnen, um Bewertungen anzuzeigen und eine neue Bewertung zu erstellen.

Als Nächstes sehen wir uns die Reviews-Komponente an (src/Reviews.js). In dieser Komponente bilden wir die Liste der Bewertungen für das ausgewählte Restaurant ab.

// src/Reviews.js

import React from 'react'
import { css } from 'glamor'

class Reviews extends React.Component {
  render() {
    const { closeModal, restaurant } = this.props
    return (
      <div {...css(styles.overlay)}>
        <div {...css(styles.container)}>
          <h1>{restaurant.name}</h1>
          {
            restaurant.reviews.items.map((r, i) => (
              <div {...css(styles.review)} key={i}>
                <p {...css(styles.text)}>{r.text}</p>
                <p {...css(styles.rating)}>Stars: {r.rating}</p>
              </div>
            ))
          }
          <p onClick={closeModal}>Close</p>
        </div>
      </div>
    )
  }
}

export default Reviews

Als Nächstes sehen wir uns die CreateRestaurant-Komponente an (src/CreateRestaurant.js). Diese Komponente enthält ein Formular, das den Formularzustand verwaltet. Die Klassenmethode createRestaurant ruft this.props.createRestaurant auf und übergibt den Formularzustand.

// src/CreateRestaurant.js

import React from 'react'
import { css } from 'glamor';

class CreateRestaurant extends React.Component {
  state = {
    name: '', city: '', photo: ''
  }
  createRestaurant = () => {
    if (
      this.state.city === '' || this.state.name === '' || this.state.photo === ''
    ) return
    this.props.createRestaurant(this.state)
    this.props.closeModal()
  }
  onChange = ({ target }) => {
    this.setState({ [target.name]: target.value })
  }
  render() {
    const { closeModal } = this.props
    return (
      <div {...css(styles.overlay)}>
        <div {...css(styles.form)}>
          <input
            placeholder='Restaurant name'
            {...css(styles.input)}
            name='name'
            onChange={this.onChange}
          />
          <input
            placeholder='City'
            {...css(styles.input)}
            name='city'
            onChange={this.onChange}
          />
          <input
            placeholder='Photo'
            {...css(styles.input)}
            name='photo'
            onChange={this.onChange}
          />
          <div
            onClick={this.createRestaurant}
            {...css(styles.button)}
          >
            <p
              {...css(styles.buttonText)}
            >Submit</p>
          </div>
          <div
            {...css([styles.button, { backgroundColor: '#555'}])}
            onClick={closeModal}
          >
            <p
              {...css(styles.buttonText)}
            >Cancel</p>
          </div>
        </div>
      </div>
    )
  }
}

export default CreateRestaurant

Als Nächstes sehen wir uns die CreateReview-Komponente an (src/CreateReview.js). Diese Komponente enthält ein Formular, das den Formularzustand verwaltet. Die Klassenmethode createReview ruft this.props.createReview auf und übergibt die Restaurant-ID und den Formularzustand.

// src/CreateReview.js

import React from 'react'
import { css } from 'glamor';
const stars = [1, 2, 3, 4, 5]
class CreateReview extends React.Component {
  state = {
    review: '', selectedIndex: null
  }
  onChange = ({ target }) => {
    this.setState({ [target.name]: target.value })
  }
  createReview = async() => {
    const { restaurant } = this.props
    const input = {
      text: this.state.review,
      rating: this.state.selectedIndex + 1,
      reviewRestaurantId: restaurant.id
    }
    try {
      this.props.createReview(restaurant.id, input)
      this.props.closeModal()
    } catch(err) {
      console.log('error creating restaurant: ', err)
    }
  }
  render() {
    const { selectedIndex } = this.state
    const { closeModal } = this.props
    return (
      <div {...css(styles.overlay)}>
        <div {...css(styles.form)}>
          <div {...css(styles.stars)}>
            {
              stars.map((s, i) => (
                <p
                  key={i}
                  onClick={() => this.setState({ selectedIndex: i })}
                  {...css([styles.star, selectedIndex === i && { backgroundColor: 'gold' }])}
                >{s} star</p>
              ))
            }
          </div>
          <input
            placeholder='Review'
            {...css(styles.input)}
            name='review'
            onChange={this.onChange}
          />
          <div
            onClick={this.createReview}
            {...css(styles.button)}
          >
            <p
              {...css(styles.buttonText)}
            >Submit</p>
          </div>
          <div
            {...css([styles.button, { backgroundColor: '#555'}])}
            onClick={closeModal}
          >
            <p
              {...css(styles.buttonText)}
            >Cancel</p>
          </div>
        </div>
      </div>
    )
  }
}

export default CreateReview

Ausführen der App

Nachdem wir unser Backend erstellt, die App konfiguriert und unsere Komponenten erstellt haben, sind wir bereit, sie zu testen

npm start

Navigieren Sie nun zu https://:3000. Herzlichen Glückwunsch, Sie haben gerade eine Full-Stack-Serverless-GraphQL-Anwendung erstellt!

Fazit

Der nächste logische Schritt für viele Anwendungen ist die Anwendung zusätzlicher Sicherheitsfunktionen, wie Authentifizierung, Autorisierung und feingranulare Zugriffskontrolle. All diese Dinge sind in den Dienst integriert. Um mehr über die Sicherheit von AWS AppSync zu erfahren, lesen Sie die Dokumentation.

Wenn Sie Hosting und eine Continuous Integration/Continuous Deployment-Pipeline für Ihre App hinzufügen möchten, sehen Sie sich die Amplify Console an.

Ich pflege auch ein paar Repositories mit zusätzlichen Ressourcen zu Amplify und AppSync: Awesome AWS Amplify und Awesome AWS AppSync.

Wenn Sie mehr über diese Philosophie der App-Erstellung mit Managed Services erfahren möchten, lesen Sie meinen Beitrag mit dem Titel „Full-stack Development in the Era of Serverless Computing.“