← Writing

Building scalable GraphQL APIs with Amplify — data modeling

Background

Caution: will try to sell you Amplify

Let's start with a bit of background — AWS Amplify is a purpose-built AWS service that lets us build scalable backends for mobile and web applications extremely fast.

Amplify:

  • Lets us build scalable backends, because under the hood Amplify uses AWS' serverless services — Cognito for authentication, AppSync for GraphQL APIs, Pinpoint for analytics, and so on. All the serverless services are built for scalability.
  • Abstracts most of the complex stuff and handles most use cases out of the box — for authentication, all you need is to run "amplify add auth" from the CLI and answer a couple of questions.
  • If some use cases are not handled out of the box, we can implement them using AWS CDK.
  • We can export the stack using CDK or CloudFormation, so next time we just need a single command to deploy the full backend.
  • Supports CI/CD for hosting and branched workflows for managing multiple environments — cid, stg, uat, prd, or whatever workflow you have.
  • Is built for setting up the backend extremely fast.

Amplify supports both REST APIs (API Gateway under the hood) and GraphQL APIs (AWS AppSync). We are going to discuss the GraphQL API.

GraphQL API and data modeling

GraphQL consists of:

  • Data model / schema — queries, mutations, subscriptions, user-defined types, and relationships.
  • Resolvers — functions whose job is, given a query/mutation/subscription, to figure out how to modify or fetch data from the configured data sources.
  • Data sources — anything from a MySQL database to an API call.

In the case of Amplify, setting up a GraphQL API means writing a schema with user-defined types only. You give Amplify a GraphQL schema and it will:

  • Generate Query, Mutation, and Subscription types for it.
  • Write resolver functions for you.
  • Set up DynamoDB as the data source.
  • Create tables in DynamoDB.

And since it's AWS, it will let you use Cognito, API keys, and other authorization methods (like Lambda) for the generated APIs — at the type level and even at the level of a single attribute. It supports user groups, and can do a bunch of ML-related things like identifying text or labels on an image and translating text, using the AWS-defined @predictions directive.

All while allowing you to customize the API's behavior by writing custom resolvers in JavaScript or VTL (advanced use cases, mostly not needed).

And the best part — you can use Amplify DataStore with it. DataStore is local storage which syncs data to the backend automatically: if you're mutating data without internet, it makes the changes locally and syncs when the connection is restored. This minimizes reload times and lets users keep working regardless of connectivity. Side note: DataStore can even be used without an AWS account.

Data modeling

We'll be making an API for a blogging platform: users own blogs, blogs have posts, and other users can comment on posts.

type User @model {
  id: ID!
  firstName: String!
  lastName: String!
  email: String!
  blogs: [Blog!] @hasMany
  comments: [Comment] @hasMany
}

type Blog @model {
  id: ID!
  name: String!
  owner: User! @belongsTo
  posts: [Post] @hasMany
}

type Post @model {
  id: ID!
  title: String!
  blog: Blog @belongsTo
  comments: [Comment] @hasMany
}

type Comment @model {
  id: ID!
  post: Post @belongsTo
  content: String!
  commentor: User! @belongsTo
}

The user has a hasMany relationship with Blog, and a Blog has an owner with belongsTo — a two-way one-to-many relationship: a user can have many blogs, and a blog belongs to a user, so we can get the user's details from the blog itself. The same shape connects blogs to posts, posts to comments, and users to comments.

Keywords in the schema:

  • ID is a unique identifier field. Omit it and Amplify makes ID the table's primary key by default; change that with the @primaryKey directive.
  • type defines a user-defined data type.
  • @model tells Amplify to create a DynamoDB table for this type.
  • @hasMany defines a one-way one-to-many relationship between two types.
  • @belongsTo makes the relationship two-way.

Deploying the API

To deploy on AWS you need an AWS account and the Amplify CLI installed and configured. Then, from your terminal:

  • amplify init — initializes the Amplify project locally. Answer the questions and move on once the project is set up.
  • amplify add api — choose GraphQL as the API type, create a blank schema, choose defaults for the rest, and paste in the schema above (it lives at amplify → backend → api → [api name] → schema.graphql).
  • amplify push — deploys the API to the backend.

Mocking

Amplify lets you mock services like the API locally — make changes, save, and they're reflected in the mock server automatically, which seriously speeds up development. Run amplify mock api and it gives you a link to the local mock server, with a full set of generated queries and mutations to play with: create a user, create a blog against that user's id, add posts and comments, then query a full blog post with its comments in one request.

One warning: avoid list queries in production — they do full table scans, and as your tables grow they get expensive. Use them for demonstration only.

Wrap-up

With just a schema file, we have a full working blog backend that covers the common use cases — and extremely complex models are possible using the provided custom directives.

Originally published on LinkedIn.

Have a product to build?

15 minutes, no slides — let's talk about it.