AWS AppSync
AWS AppSync Developer Guide

Building a ReactJS Client App

AWS AppSync integrates with the Apollo GraphQL client for building client applications. AWS provides Apollo plugins for offline support, authorization, and subscription handshaking. You can use the Apollo client directly, or you can use it with some of the client helpers provided in the AWS AppSync SDK.

This tutorial shows you how to use AWS AppSync with React Apollo, which uses ReactJS constructs and patterns with GraphQL. For the latest AWS AppSync SDK documentation, including API definitions and sample apps, see aws-mobile-appsync-sdk-js on GitHub.

Alternatively for a simple, lightweight GraphQL client that is integrated into the AWS ecosystem for authentication, object storage, analytics, and other features please see AWS Amplify and the GraphQL client.

Before You Begin

This tutorial is set up for a sample API using the schema from the DynamoDB resolvers tutorial. To follow along with the complete flow, you can optionally walk through that tutorial first. If you want to do more customization of GraphQL resolvers, such as those that use DynamoDB, see the Resolver Mapping Template Reference. The application uses the following starting schema:

schema { query: Query mutation: Mutation } type Mutation { addPost(id: ID! author: String! title: String content: String! url: String!): Post! updatePost(id: ID! author: String! title: String content: String url: String expectedVersion: Int!): Post! deletePost(id: ID!, expectedVersion: Int): Post } type Post { id: ID! author: String! title: String content: String url: String ups: Int downs: Int version: Int! } type PaginatedPosts { posts: [Post!]! nextToken: String } type Query { allPost(count: Int, nextToken: String): PaginatedPosts! getPost(id: ID!): Post }

This schema defines a Post type and operations to add, get, update, and delete Post objects.

Get the GraphQL API Endpoint

After you create your GraphQL API, you need to get the API endpoint (URL) so you can use it in your client application. You can get the API endpoint in either of the following ways:

  • In the AWS AppSync console, choose Home and then choose GraphQL URL to see the API endpoint.

  • Run the following CLI command:

aws appsync get-graphql-api --api-id $GRAPHQL_API_ID

The following instructions show how you can use AWS_IAM for client authorization. In the AWS AppSync console, choose Settings on the left, and then choose AWS_IAM. For more information about authorization modes, see Security.

Download a Client Application

To show you how to use AWS AppSync, we first review a React application with just a local array of data. Then, we add AWS AppSync capabilities to it. To begin, download a sample application where we can add, update, and delete posts.

Understanding the React Sample App

The React sample app has three major files:

  • ./src/App.js: The main entry point of the application. It renders the main application shell with two components named AddPost and AllPosts, and has a local array of data named posts which is passed as a prop to the other components.

  • ./src/Components/AddPost: A React component that contains a form that enables a user to enter new information about a post, such as the author and title.

  • ./src/Components/AllPosts: A React component that lists all existing posts from the posts array that App.js created. It enables you to edit or delete existing posts.

Run your app as follows, and test it to verify that it works:

yarn && yarn start

Import the AWS AppSync SDK into Your App

In this section, you'll add AWS AppSync to your existing app.

Add the following dependencies to your application:

yarn add react-apollo graphql-tag aws-sdk

Next, add in the AWS AppSync SDK, including the React extensions as follows:

yarn add aws-appsync yarn add aws-appsync-react

From the AWS AppSync console, go to your GraphQL API integration page at the root of the navigation bar on the left ). At the bottom of the page, choose JavaScript. Next, click Download Config and save the aws-exports.js configuration file into ./src.

To interact with AWS AppSync, your client needs to define GraphQL queries and mutations. This is commonly done in separate files, as follows:

mkdir ./src/Queries touch ./src/Queries/AllPostsQuery.js touch ./src/Queries/DeletePostMutation.js touch ./src/Queries/NewPostMutation.js touch ./src/Queries/UpdatePostMutation.js

Edit and save AllPostsQuery.js:

import gql from 'graphql-tag'; export default gql` query AllPosts { allPost { posts { __typename id title author version } } }`;

Edit and save DeletePostMutation.js:

import gql from 'graphql-tag'; export default gql` mutation DeletePostMutation($id: ID!, $expectedVersion: Int!) { deletePost(id: $id, expectedVersion: $expectedVersion) { __typename id author title version } }`;

Edit and save NewPostMutation.js:

import gql from 'graphql-tag'; export default gql` mutation AddPostMutation($id: ID!, $author: String!, $title: String!) { addPost( id: $id author: $author title: $title content: " " url: " " ) { __typename id author title version } }`;

Edit and save UpdatePostMutation.js:

import gql from 'graphql-tag'; export default gql` mutation UpdatePostMutation($id: ID!, $author: String, $title: String, $expectedVersion: Int!) { updatePost( id: $id author: $author title: $title expectedVersion: $expectedVersion ) { __typename id author title version } }`;

Edit your App.js file, as follows:

import AWSAppSyncClient from "aws-appsync"; import { Rehydrated } from 'aws-appsync-react'; import { AUTH_TYPE } from "aws-appsync/lib/link/auth-link"; import { graphql, ApolloProvider, compose } from 'react-apollo'; import * as AWS from 'aws-sdk'; import AppSync from './aws-exports.js'; import AllPostsQuery from './Queries/AllPostsQuery'; import NewPostMutation from './Queries/NewPostMutation'; import DeletePostMutation from './Queries/DeletePostMutation'; import UpdatePostMutation from './Queries/UpdatePostMutation';

After all the import statements, add the following code:

const client = new AWSAppSyncClient({ url: AppSync.graphqlEndpoint, region: AppSync.region, auth: { type: AUTH_TYPE.API_KEY, apiKey: AppSync.apiKey, // type: AUTH_TYPE.AWS_IAM, // Note - Testing purposes only /*credentials: new AWS.Credentials({ accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY })*/ // Amazon Cognito Federated Identities using AWS Amplify //credentials: () => Auth.currentCredentials(), // Amazon Cognito user pools using AWS Amplify // type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, // jwtToken: async () => (await Auth.currentSession()).getIdToken().getJwtToken(), }, });

You can switch the AUTH_TYPE value to use API keys, IAM (including short-term credentials from Amazon Cognito federated identities), or Amazon Cognito user pools. We recommend that you use either IAM or Amazon Cognito user pools after onboarding with an API key. The previous code shows how to use the default configuration of AWS AppSync with an API key, referencing the aws-exports.js file you downloaded. When you're ready to add other authorization methods to your application, you can use the AWS Amplify library to quickly add these capabilities to your application. The corresponding AWS Amplify methods for the AWS AppSync client constructor are included previously. An import of the library with configuration would look similar to the following:

import Amplify, { Auth } from 'aws-amplify'; import { withAuthenticator } from 'aws-amplify-react'; Amplify.configure(awsmobile); //...code const AppWithAuth = withAuthenticator(App, true);

For more information about using AWS Amplify, see the library documentation.

Replace the App component entirely, so that it looks like the following:

class App extends Component { render() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <h1 className="App-title">Welcome to React</h1> </header> <p className="App-intro"> To get started, edit <code>src/App.js</code> and save to reload. </p> <NewPostWithData /> <AllPostsWithData /> </div> ); } }

You can also delete the posts variable in your code because the app state comes from AWS AppSync.

At the bottom of your App.js file, define the following higher-order component (HOC):

const AllPostsWithData = compose( graphql(AllPostsQuery, { options: { fetchPolicy: 'cache-and-network' }, props: (props) => ({ posts: props.data.allPost && props.data.allPost.posts, }) }), graphql(DeletePostMutation, { props: (props) => ({ onDelete: (post) => props.mutate({ variables: { id: post.id, expectedVersion: post.version }, optimisticResponse: () => ({ deletePost: { ...post, __typename: 'Post' } }), }) }), options: { refetchQueries: [{ query: AllPostsQuery }], update: (proxy, { data: { deletePost: { id } } }) => { const query = AllPostsQuery; const data = proxy.readQuery({ query }); data.allPost.posts = data.allPost.posts.filter(post => post.id !== id); proxy.writeQuery({ query, data }); } } }), graphql(UpdatePostMutation, { props: (props) => ({ onEdit: (post) => { props.mutate({ variables: { ...post, expectedVersion: post.version }, optimisticResponse: () => ({ updatePost: { ...post, __typename: 'Post', version: post.version + 1 } }), }) } }), options: { refetchQueries: [{ query: AllPostsQuery }], update: (dataProxy, { data: { updatePost } }) => { const query = AllPostsQuery; const data = dataProxy.readQuery({ query }); data.allPost.posts = data.allPost.posts.map(post => post.id !== updatePost.id ? post : { ...updatePost }); dataProxy.writeQuery({ query, data }); } } }) )(AllPosts); const NewPostWithData = graphql(NewPostMutation, { props: (props) => ({ onAdd: post => props.mutate({ variables: post, optimisticResponse: () => ({ addPost: { ...post, __typename: 'Post', version: 1 } }), }) }), options: { refetchQueries: [{ query: AllPostsQuery }], update: (dataProxy, { data: { addPost } }) => { const query = AllPostsQuery; const data = dataProxy.readQuery({ query }); data.allPost.posts.push(addPost); dataProxy.writeQuery({ query, data }); } } })(AddPost);

Finally, replace export default App with the ApolloProvider as follows:

const WithProvider = () => ( <ApolloProvider client={client}> <Rehydrated> <App /> </Rehydrated> </ApolloProvider> ); export default WithProvider;

Test Your Application

yarn start

Open a webpage and add, remove, edit, and delete data. If you're using Chrome developer tools, you can use the network conditioning tool for offline testing.

Offline Settings

There are important considerations that you need to account for if you want an optimistic UI for an application, where data can be manipulated when the device is in an offline state. Many of these settings are documented in the official Apollo documentation, however, we call out several of them here that you should configure.

First, know that the AWS AppSync client allows you to disable offline capabilities if you simply want to use GraphQL in an always-online scenario. To do this, you pass an additional option when instantiating your client, named disableOffline, as follows:

const client = new AWSAppSyncClient({ url: AppSync.graphqlEndpoint, region: AppSync.region, auth: { type: AUTH_TYPE.API_KEY, apiKey: AppSync.apiKey, }, disableOffline: true });
  • fetchPolicy: This option enables you to specify how a query interacts with the network versus local in-memory caching. AWS AppSync persists this cache to a platform-specific storage medium. If you are using the AWS AppSync client in offline scenarios (disableOffline:false), you MUST set this value to cache-and-network as follows:

    options: { fetchPolicy: 'cache-and-network' }
  • optimisticResponse: This option enables you to pass a function or an object to a mutation for updating your UI before the server responds with the result. You need this for offline scenarios (and for slower networks) to ensure that the UI is updated when the device has no connectivity. Optionally, you can also use this if you have set disableOffline:true. For example, if you wanted to add a new object to a list, you could use the following:

    onAdd: post => props.mutate({ variables: post, optimisticResponse: () => ({ addPost: { __typename: 'Post', ups: 1, downs: 1, content: '', url: '', version: 1, ...post } }), })

Typically, you use optimisticResponse in conjunction with the update option for React Apollo's component, which can trigger during an offline mutation. If you want the UI to update offline for a specific query, you need to specify that query as part of the readQuery and writeQuery options on the cache, as shown following:

options: { refetchQueries: [{ query: AllPostsQuery }], update: (dataProxy, { data: { addPost } }) => { const query = AllPostsQuery; const data = dataProxy.readQuery({ query }); data.allPost.posts.push(addPost); dataProxy.writeQuery({ query, data }); } }

When this happens, the AWS AppSync persistent store update automatically in response to the Apollo cache update. Upon network reconnection, it will synchronize with your GraphQL endpoint. You could also modify more than one query when offline, in which case you could run the process multiple times in the same update block.

Make Your Application Real Time

Edit your schema with the subscription type, as follows:

schema { query: Query mutation: Mutation subscription: Subscription } type Mutation { addPost(id: ID! author: String! title: String content: String! url: String!): Post! updatePost(id: ID! author: String! title: String content: String url: String expectedVersion: Int!): Post! deletePost(id: ID!, expectedVersion: Int): Post } type Post { id: ID! author: String! title: String content: String url: String ups: Int downs: Int version: Int! } type PaginatedPosts { posts: [Post!]! nextToken: String } type Query { allPost(count: Int, nextToken: String): PaginatedPosts! getPost(id: ID!): Post } type Subscription { newPost: Post @aws_subscribe(mutations:["addPost"]) }

Notice that the @aws_subscribe specifies which mutations trigger a subscription. You can add more mutations in this array to meet your application needs.

The subscription type newPost needs to be passed into an option (named updateQuery) of the React Apollo client to update your UI dynamically when a subscription is received. Ensure that this field name matches the subscription type in the following example code.

In your App.js file, edit the AllPostsWithData HOC to include subscribeToNewPost in the props field, as follows:

const AllPostsWithData = compose( graphql(AllPostsQuery, { options: { fetchPolicy: 'cache-and-network' }, props: (props) => ({ posts: props.data.allPost && props.data.allPost.posts, // START - NEW PROP : subscribeToNewPosts: params => { props.data.subscribeToMore({ document: NewPostsSubscription, updateQuery: (prev, { subscriptionData: { data : { newPost } } }) => ({ ...prev, allPost: { posts: [newPost, ...prev.allPost.posts.filter(post => post.id !== newPost.id)], __typename: 'PaginatedPosts' } }) }); }, // END - NEW PROP } }) }), ...//more code
touch src/Queries/NewPostsSubscription.js
import gql from 'graphql-tag'; export default gql` subscription NewPostSub { newPost { __typename id title author version } }`;

Add the following import statement at the top of your App.js file:

import NewPostsSubscription from './Queries/NewPostsSubscription';

Add the following lifecycle method to your AllPosts component in AllPosts.jsx:

componentWillMount(){ this.props.subscribeToNewPosts(); }

Now try running your app again by typing yarn start. Add a new post via the console, with a mutation on addPost. You should see real-time data appear in your client application.

Complex Objects

Many times you might want to create logical objects that have more complex data, such as images or videos, as part of their structure. For example, you might create a Person type with a profile picture or a Post type that has an associated image. With AWS AppSync, you can model these as GraphQL types, referred to as complex objects. If any of your mutations have a variable with bucket, key, region, mimeType and localUri fields, the SDK uploads the file to Amazon S3 for you.

For a complete working example of this feature, see aws-amplify-graphql on GitHub.

First, to use complex objects you need AWS Identity and Access Management credentials for reading and writing to Amazon S3. These can be separate from the other auth credentials you use in your AWS AppSync client. Credentials for complex objects are set using the complexObjectsCredentials parameter, which you can use with AWS Amplify like the following:

const client = new AWSAppSyncClient({ url: ENDPOINT, region: REGION, auth: { .. }, complexObjectsCredentials: () => Auth.currentCredentials(), });

Edit your schema to add the S3Object and S3ObjectInput types, as follows:

schema { query: Query mutation: Mutation subscription: Subscription } type Mutation { addPost(id: ID! author: String! title: String content: String! url: String! file: S3ObjectInput): Post! updatePost(id: ID! author: String! title: String content: String url: expectedVersion: Int!): Post! deletePost(id: ID!, expectedVersion: Int): Post } type Post { id: ID! author: String! title: String content: String url: String ups: Int downs: Int file: S3Object version: Int! } type PaginatedPosts { posts: [Post!]! nextToken: String } type S3Object { bucket: String! key: String! region: String! } input S3ObjectInput { bucket: String! key: String! region: String! localUri: String mimeType: String } type Query { allPost(count: Int, nextToken: String): PaginatedPosts! getPost(id: ID!): Post } type Subscription { newPost: Post @aws_subscribe(mutations:["addPost"]) }

Edit your ./src/Components/AddPost.jsx file, as follows:

import React, { Component } from "react"; import { v4 as uuid } from 'uuid'; export default class AddPost extends Component { constructor(props) { super(props); this.state = this.getInitialState(); } static defaultProps = { onAdd: () => null } getInitialState = () => ({ id: '', title: '', author: '', file: null, }); handleChange = (field, event) => { const { target: { value } } = event; this.setState({ [field]: value }); } handleAdd = () => { const { title, author, file: selectedFile } = this.state; let file; if (selectedFile) { const { name, type: mimeType } = selectedFile; const [, , , extension] = /([^.]+)(\.(\w+))?$/.exec(name); const bucket = '[YOUR BUCKET]'; const key = [uuid(), extension].filter(x => !!x).join('.'); const region = '[YOUR REGION]'; file = { bucket, key, region, mimeType, localUri: selectedFile, }; } this.setState(this.getInitialState(), () => { this.props.onAdd({ title, author, content: 'hardcoded', file }); }); } handleCancel = () => { this.setState(this.getInitialState()); } render() { return ( <fieldset > <legend>Add new Post</legend> <div> <label>ID<input type="text" placeholder="ID" value={this.state.id} onChange={this.handleChange.bind(this, 'id')} /></label> </div> <div> <label>Title<input type="text" placeholder="Title" value={this.state.title} onChange={this.handleChange.bind(this, 'title')} /></label> </div> <div> <label>Author<input type="text" placeholder="Author" value={this.state.author} onChange={this.handleChange.bind(this, 'author')} /></label> </div> <div> <label>File<input type="file" onChange={this.handleChange.bind(this, 'file')} /></label> </div> <div> <button onClick={this.handleAdd}>Add new post</button> <button onClick={this.handleCancel}>Cancel</button> </div> </fieldset> ); } }

Now try running your app again by typing yarn start. Add a new post via the console, with a mutation on addPost. Your file should be uploaded to Amazon S3 before doing your mutation.

Conflict Resolution

When clients make a mutation, either online or offline, they can send a version number with the payload (named expectedVersion) for AWS AppSync to check before writing to Amazon DynamoDB. A DynamoDB resolver mapping template can be configured to perform conflict resolution in the cloud, which you can learn about in Resolver Mapping Template Reference for DynamoDB. If the service determines it needs to reject the mutation, data is sent to the client and you can optionally run an additional callback to perform client-side conflict resolution.

For example, suppose you had a mutation with DynamoDB set for checking the version, and the client sent expectedVersion:0, as in the following example:

graphql(UpdatePostMutation, { props: (props) => ({ onEdit: (post) => { props.mutate({ variables: { ...post, expectedVersion: 0 }, optimisticResponse: () => ({ updatePost: { ...post, __typename: 'Post', version: post.version + 1 } }), }) } }),...more code

This would fail the version check because 0 would be lower than any of the current values. You can then define a custom callback conflict resolver. A custom conflict resolver receives the following variables:

  • mutation: GraphQL statement of a mutation

  • mutationName: Optional if a name of a mutation is set on a GraphQL statement

  • variables: Input parameters of the mutation

  • data: Response from AWS AppSync of actual data in DynamoDB

  • retries: Number of times a mutation has been retried

For example, you could have the following custom callback conflict resolver:

const conflictResolver = ({ mutation, mutationName, variables, data, retries }) => { switch (mutationName) { case 'UpdatePostMutation': return { ...variables, expectedVersion: data.version, }; default: return false; } }

In the previous example, you can do a logical check on the mutationName and then rerun the mutation with the correct version that AWS AppSync returned.

Note: We recommend doing this only in rare cases. Usually, we recommend that you let the AWS AppSync service define conflict resolution to prevent race conditions from occurring. If you don't want to retry, simply return DISCARD.

Now, to use this callback, pass it into the AWS AppSync client instantiation as follows:

const client = new AWSAppSyncClient({ url: awsconfig.ENDPOINT, region: awsconfig.REGION, auth: authInfo, conflictResolver, });