AWS AppSync architecture
Part 1: Introduction: GraphQL endpoints with API Gateway + AWS Lambda Part 2: AppSync Backend: AWS Managed GraphQL ServicePart 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
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 :
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 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
Now, the exciting stuff begins! 💃
The basic structure of this App is created using [create-react-app](https://github.com/facebook/create-react-app)
. Also, we are using [styled-components](https://github.com/styled-components/styled-components)
to make our App look fancy 💅Given below are the five main components of this application.
In order to make it all work, we take advantage of specific GraphQL operations:
Various Components of mini Twitter App
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) {namelocationdescriptionfollowing}}`;
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.
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 {tweettweet_id}nextToken}}}`;
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_idtweet}}`**;
Now, we need to add two more functionalities in our component, also explained in this post.
optimisticResponse
defines the new response you would like to have available in the update function.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 👓
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 {__typenametweet_idtweet}}`**;
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);
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 {tweettweet_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
);
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:** awsruntime: nodejs6.10stage: devregion: us-east-1
plugins: - serverless-finch
custom:client:bucketName: <unique bucket name>distributionFolder: build
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