Siddharth Gupta

@sid88in

Running a scalable & reliable GraphQL endpoint with Serverless

Part 3: AppSync Frontend: AWS Managed GraphQL Service

AWS AppSync architecture
Part 1: Introduction: GraphQL endpoints with API Gateway + AWS Lambda 
Part 2: AppSync Backend: AWS Managed GraphQL Service
Part 3: AppSync Frontend: AWS Managed GraphQL Service (this post)
Part 4: Serverless AppSync Plugin: New Features (Latest!!!)
AWS AppSync is a fully managed Serverless GraphQL service for real-time data queries, synchronization, communications and offline programming features.” — Part 2

Introduction

In this post, we will learn about building mini Twitter App’s client components using ReactJS and AWS AppSync. In particular, I will focus on :

  • User Authentication with AWS Amplify.
  • Mini Twitter App Components.
  • GraphQL Subscriptions.
  • GraphQL Mutations with Optimistic UI and Offline Support.
  • Serverless Client Deployment with Netlify and S3.

Let’s get started! 🏃

Note 1: In Part 2 we have created mini Twitter App’s backend GraphQL API using AWS AppSync, DynamoDB, ElasticSearch and AWS Lambda. We also deployed the API using new serverless-appsync-plugin.
Note 2: You can quickly get started with this App in the serverless-graphql repository using yarn start. Please make sure configs are set properly.
Note 3: AppSync Client also has SDK’s for native iOS, web, and React Native, with Android but in this post, we are going build a React JS App with JS SDK.

AppSync Client + AWS Amplify

AppSync Client uses Apollo Client 2.0 under the hood to simplify user authentication, manage offline logic and support real-time subscriptions.

On the other hand, you can use AppSync Client with AWS Amplify to simplify user authentication workflows in your application 🔑. AppSync provides authentication using API Key, Cognito User Pools or AWS IAM policies and AWS Amplify complements the same with methods provided in Auth Class for user sign-up, sign-in, password confirmation and sign-out.

import Amplify, { Auth } from 'aws-amplify';
import { withAuthenticator } from 'aws-amplify-react/dist/Auth';
import AWSAppSyncClient from 'aws-appsync';
import { ApolloProvider } from 'react-apollo';
const client = new AWSAppSyncClient({
url: 'https://xxxx.appsync-api.us-east-1.amazonaws.com/graphql',
region: 'us-east-1',
auth: {
    // AWS Cognito User Pool
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
jwtToken: async () =>
(await Auth.currentSession()).getIdToken().getJwtToken(),

// API KEY
type: AUTH_TYPE.API_KEY,
apiKey: 'xxxxxxxxxxxxx',
    // AWS IAM
type: AUTH_TYPE.AWS_IAM
credentials: () => Auth.currentCredentials(),
   },
});
const WithProvider = () => (
<Router>
<ApolloProvider client={client}>
</Router>
);
export default withAuthenticator(WithProvider);

As seen in App.js code above, adding authentication to your application is as easy as wrapping your App’s main component with withAuthenticator higher order component. You can use aws-appsync-react and aws-amplify-react packages by AWS for all of these integrations.

Note: For this demo, I am using AWS Cognito User Pool for User Authentication and have created two test users (sidg_sid and nikgraf) in Cognito. Once the user is logged in, their session is persisted in localStorage by Amplify. So the user can leave the page, come back, and still be logged in! You can find more details in this post by Nader Dabit.
User Authentication with AWS AppSync + AWS Amplify + AWS Cognito

Mini Twitter App Components

Now, the exciting stuff begins! 💃

The basic structure of this App is created using create-react-app . Also, we are using styled-components to make our App look fancy 💅Given below are the five main components of this application.

  • UserLogin: users can log in or sign out from this App (previous section).
  • ProfileInfo: retrieve basic user profile info from DynamoDB.
  • ProfileTweets: retrieve a list of tweets from ElasticSearch.
  • TweetForm: users can send a tweet.
  • TweetSearch: users can search through a corpus of all tweets by keywords.

In order to make it all work, we take advantage of specific GraphQL operations:

  • Queries — fetch profile info and list of tweets for a given user.
  • Mutations — create and delete a tweet for a given user.
  • Subscriptions — followers of a given user can see his new tweets.
Various Components of mini Twitter App

Profile Info Component:

In this section, you will see how to wire up this component using ProfileInfoQuery , graphql from react-apollo .

The GraphQL schema for this App defines getUserInfo. The resolver for the query given below fetches data from DynamoDB for a given user handle.

export const ProfileInfoQuery = gql`
query
ProfileInfoQuery($handle: String!) {
getUserInfo(handle: $handle) {
name
location
description
following
}
}
`
;

The value of handle is parsed from JWT token and is available in context.identity.username or it can be provided as an input context.arguments.handle . The query above is resolved using the following mapping template in AppSync Backend.

{
"version" : "2017-02-28",
"operation" : "Query",
"query" : {
"expression": "handle = :handle",
"expressionValues" : {
":handle" : {
"S" : "${context.identity.username}"
}
}
}
}

At the end, ProfileInfoComponent is:

import React from 'react';
import { graphql } from 'react-apollo';
import { ProfileInfoQuery } from '../queries';
const ProfileInfo = ({ data: { loading, getUserInfo }}) => {
if (loading) { return ( <p>Loading ...</p> ); }
  return ( <div> <h4> {getUserInfo.name} </h4> </div> );
};
export default graphql(ProfileInfoQuery, {
options: props => ({
variables: {
handle: props.handle,
},
}),
})(ProfileInfo);
Note: mini Twitter App’s GraphQL schema and resolvers are explained in Part 2.

Profile Tweets Component:

Data for this component is also retrieved from getUserInfo defined in AppSync schema. The resolver for this query hits ElasticSearch tweet index and retrieves tweets by handle.

export const ProfileTweetsQuery = gql`
query
ProfileTweetsQuery {
getUserInfo {
tweets(limit: 10) {
items {
tweet
tweet_id
}
nextToken
}
}
}
`
;

Optimistic Response and Offline Support

Now, lets imagine the following scenario:

Scenario: You are coming back home after long day at work. You take the train from Station A to Station B. Now, you are also tweeting your thoughts on your favorite topic of interest but all of a sudden train goes through a tunnel, and now you are having network connectivity issues. How will your App handle this?

In this possible scenario, a user would expect the App to behave normally (oh yeah! users expect a lot 😉and it doesn’t take them a lot of time to click on that delete App button 💁). This is where Optimistic Response and Offline Support come to the rescue when backend is unreachable.

Our next component, TweetForm handles the scenario explained above. In this case, create tweet mutation puts a tweet record in ElasticSearch index.

export const AddTweetMutation = gql`
mutation AddTweetMutation(
$tweet: String!
) {
createTweet(
tweet: $tweet
) {
tweet_id
tweet
}
}
`
;

Now, we need to add two more functionalities in our component, also explained in this post.

  1. optimisticResponse defines the new response you would like to have available in the update function.
  2. update takes two arguments, the proxy (which allows you to read from the cache) and the data you would like to use to make the update. We read the current cache (proxy.readQuery), add it our new item to the array of items, and then write back to the cache, which updated our UI.
export default graphql(AddTweetMutation, {
props: ({ mutate }) => ({
addTweet: tweet => {
return mutate({
variables: {
tweet,
},
optimisticResponse: () => ({
createTweet: {
tweet,
tweet_id: uuid(),
__typename: 'Tweet',
},
}),
update: (proxy, { data: { createTweet } }) => {
const data = proxy.readQuery({
query: ProfileTweetsQuery,
variables: {
tweet,
},
});
data.meInfo.tweets.items.push(createTweet);
proxy.writeQuery({
query: ProfileTweetsQuery,
data,
variables: {
tweet,
},
});
},
});
},
}),
})(TweetFormComponent);
and.. boom! You can see the magic yourself 👓

Let’s see how all this real-time stuff works:

The best part? All you need to get subscriptions working in the backend is to extend your GraphQL schema with 4 lines of code:
type Subscription {
addTweet: Tweet
@aws_subscribe(mutations: [“createTweet”]
}
Scenario: Let’s say we have two users (sidg_sid and nikgraf) following each other. In this case, both the users are subscribed to each other’s tweets. As shown below, when user sidg_sid sends a tweet it is immediately pushed to all his followers including nikgraf and vice-versa.
Real Time Subscriptions

Subscriptions in AWS AppSync are invoked as a response to a mutation. In addition, they are handled automatically by the AWS AppSync client SDK using MQTT over Websockets as the network protocol between the client and service. The following subscription is invoked every time a new tweet is added.

export const AddTweetSubscription = gql`
subscription AddTweetSubscription {
addTweet {
__typename
tweet_id
tweet
}
}
`
;

export default {
AddTweetSubscription,
};

We now add this subscription to Profile Tweets Component by calling subscribeToMore function with AddTweetSubscription and user handle . The updateQuery adds a new tweet in the list of previous tweets for a given user.

const tweetsQuery = graphql(ProfileTweetsQuery, {
options: props => ({
variables: { ...variables, handle: props.handle },
fetchPolicy: 'cache-and-network',
}),
props: props => ({
...props,
subscribeToNewTweets: params =>
props.data.subscribeToMore({
document: AddTweetSubscription,
variables: params,
updateQuery: (prev, { subscriptionData: { data: { addTweet } } }) => {
return {
...prev,
getUserInfo: {
...prev.getUserInfo,
tweets: {
items: [addTweet, ...prev.getUserInfo.tweets.items],
},
},
};
},
}),
}),
});

export default compose(tweetsQuery)(ProfileTweetsComponent);

Search all Tweets Component

Last but not the least, users can also search through corpus of tweets by keyword. The resolver for this query maps to an ElasticSearch query in the backend.

export const SearchTweetsQuery = gql`
query UserQuery($keyword: String!) {
searchAllTweetsByKeyword(keyword: $keyword) {
items {
tweet
tweet_id
}
}
}
`
;
ElasticSearch Query

At the end, SearchTweetsComponent is:

import React from 'react';
import { graphql } from 'react-apollo';
import { SearchTweetsQuery } from '../queries';
const Search = ({ data: { loading, searchAllTweetsByKeyword }}) => {
if (loading) { return ( <p>Loading ...</p> ); }
return (
<Container>
{searchAllTweetsByKeyword.items.map((item, index) => (
<Tweet key={index}>{item.tweet}</Tweet>
))}
</Container>
);
};
export default graphql(SearchTweetsQuery, {
options: props => ({
variables: {
handle: props.handle,
},
}),
})(Search);

Serverless Client Deployment with Netlify and/or S3

Deploy Netlify: yarn build && netlify deploy build
Deploy S3: yarn build && serverless client deploy
service: serverless-graphql-client

frameworkVersion: ">=1.21.0 <2.0.0"

provider:
name:
aws
runtime: nodejs6.10
stage: dev
region: us-east-1

plugins:
- serverless-finch

custom:
client:
bucketName:
<unique bucket name>
distributionFolder: build

Special Thanks

Nik Graf for working together to implement the client components. 
Manuel and Nader for helping and reviewing the code.

Last but not the least, to everyone for encouraging me to write more and appreciating previous blogs.

I would like to end this blog with one of my favourite quotes —

“Imagination is more important than knowledge. For knowledge is limited, whereas imagination embraces the entire world, stimulating progress, giving birth to evolution.” — Albert Einstein

More by Siddharth Gupta

Topics of interest

More Related Stories