GraphQL Schema mit AppSync und AWS CDK programmatisch erstellen

June 21, 2022

Mit AWS AppSync lassen sich unkompliziert serverless GraphQL APIs betreiben. Dank einfacheren Integrationen von Amazon DynamoDB, RDS, OpenSearch und Lambda können beliebige Datenquellen genutzt werden und über ein einheitliches Interface abgefragt werden. Wie in unserem Rundumschlag zu AppSync und GraphQL aus 2019 zu lesen ist, existiert für GraphQL APIs oft ein sog. schema first Ansatz; Das Schema einer AppSync API kann mit dem CDK aber auch programmatisch erstellt werden.

Über AppSync und GraphQL haben wir schon viel geschrieben: Wie sich AppSync mit NextJS benutzen lässt und Azure AD als Identity Provider verwendet werden kann, wie AppSync ohne AWS Lambda Zugriff auf DynamoDB erhält, oder den erwähnten Rundumschlag zu AppSync und GraphQL aus 2019.

Programmatisches Schema

Anders als beim vorgestellten Ansatz mit schema first existiert das GraphQL Schema nicht als Datei im jeweiligen Projekt; das folgende Schema ist das Ergebnis seiner Implementierung. Als theoretisches Beispiel dient hier eine API zum Abfragen einer Bildergallerie inklusive Liste an enthaltenen Dateien:

type Image {
  id: ID!
  file: String!
}

type ImageList {
  items: [Image!]!
  cursor: String
}

type Gallery {
  id: ID!
  name: String!
  images(limit: Int, cursor: String): ImageList!
}

type Query {
  gallery(id: ID!): Gallery!
}

schema {
  query: Query
}

GraphQL und AWS CDK

Mit dem AWS Cloud Development Kit wird zunächst die AWS AppSync GraphQL API erstellt und danach die notwendigen Typdefinitionen für Image, ImageList und Gallery zum Schema hinzugefügt:

import * as appsync from "@aws-cdk/aws-appsync-alpha";

// AWS AppSync GraphQL API
const api = new appsync.GraphqlApi(this, "api", {
  name: `Gallery`,
  schema: new appsync.Schema(),
});

// ObjectType for Image
const typeImage = new appsync.ObjectType("Image", {
  definition: {
    id: appsync.GraphqlType.id({ isRequired: true }),
    file: appsync.GraphqlType.string({ isRequired: true }),
  },
});

// ObjectType for ImageList
const typeImageList = new appsync.ObjectType(`ImageList`, {
  definition: {
    items: typeImage.attribute({
      isList: true,
      isRequired: true,
      isRequiredList: true,
    }),
    cursor: appsync.GraphqlType.string(),
  },
});

// ObjectType for Gallery
const typeGallery = new appsync.ObjectType("Gallery", {
  definition: {
    id: appsync.GraphqlType.id({ isRequired: true }),
    name: appsync.GraphqlType.string({ isRequired: true }),
  },
});

// Extend schema with ObjectType definitions
api.schema.addType(typeImage);
api.schema.addType(typeImageList);
api.schema.addType(typeGallery);

Neben der starren Struktur der Objekt Typen und ihrer Felder mit einfachen Typen, gibt es natürlich auch Resolver um komplexere Datenstrukturen abzufragen bzw. die genannten Datenquellen (wie Amazon DynamoDB, RDS, OpenSearch und Lambda) zu integrieren.

Für das Beispiel benötigt die AppSync API zwei Resolver:

  • Gallery.images für die Liste an Dateien einer Gallery, und
  • Query.gallery um eine Gallery mittels ihrer ID abzufragen.

Beide Resolver benötigen sogenannte Velocity Templates, um die Anfragen für die konfigurierten Datenquellen zu formatieren bzw. um deren Rückgabewerte für die AppSync API aufzubereiten. Für das Beispiel sind zwei Templates ohne wirkliche Funktionalität ausreichend:

// Example template for request mapping
const dummyRequestTemplate = appsync.MappingTemplate.fromString(`
  {
    "version": "2018-05-29",
    "payload": $utils.toJson($context.arguments)
  }
`);

// Example template for response mapping
const dummyResponseTemplate = appsync.MappingTemplate.fromString(`
  #if($context.error)
    $util.error($context.error.message, $context.error.type, $context.result)
  #end

  $util.toJson($context.result)
`);

Mit den beispielhaften Velocity Templates lassen sich nun die Resolver erstellen:

// AppSync Resolver for Query.gallery(id: ID!)
const resolveGallery = new appsync.ResolvableField({
  returnType: typeGallery.attribute({ isRequired: true }),
  args: {
    id: appsync.GraphqlType.id({ isRequired: true }),
  },
  dataSource: api.addNoneDataSource("DataSourceGallery"),
  requestMappingTemplate: dummyRequestTemplate,
  responseMappingTemplate: dummyResponseTemplate,
});

// AppSync Resolver for Gallery.files(limit: Int, cursor: String)
const resolveImageList = new appsync.ResolvableField({
  returnType: typeImageList.attribute({ isRequired: true }),
  args: {
    limit: appsync.GraphqlType.int(),
    cursor: appsync.GraphqlType.string(),
  },
  dataSource: api.addNoneDataSource("DataSourceImages"),
  requestMappingTemplate: dummyRequestTemplate,
  responseMappingTemplate: dummyResponseTemplate,
});

Im letzten Schritt müssen die erstellten Resolver noch mit dem Datentypen verbunden werden:

// Use resolver for Gallery.images
typeGallery.addField({
  fieldName: "images",
  field: resolveImageList,
});

// Use resolver for Query.gallery
api.schema.addQuery("gallery", resolveGallery);

Mit einem Deployment durch cdk deploy wird nun auf Basis der konfigurierten Typen und Resolver das GraphQL Schema erstellt und kann über die AWS AppSync Console genutzt werden:

AWS AppSync Console

photo of Sebastian

Sebastian is a Senior Cloud Consultant at superluminar GmbH, AWS Serverless Hero and AWS Certified Solutions Architect. He writes here in German about AWS, Serverless, Software development, Go, TypeScript and React. English Articles can be found on his Website sbstjn.com and he can be found on Twitter under @sbstjn.