Build a serverless React + GraphQL app with AWS Amplify and AppSync

This post is going to be a tad different and longer than what you are used to but I promise, it’s going to be an interesting one. We are going to build a serverless React + GraphQL Web app with AWS amplify and AppSync.

What is Aws AppSync?

Aws AppSync helps us create a serverless backend for Android or IOS or Web apps. It integrates with Amazon DynamoDB, Elasticsearch, Cognito, and Lambda, enabling you to create sophisticated applications, with virtually unlimited throughput and storage, that scale according to your business needs.

AppSync also enables real-time subscriptions as well as offline access to app data.

When an offline device reconnects, AppSync will syncs only the updates that occurred while the device was offline and not the entire database.

How does AppSync Work?

We’ll create our GraphQL schema by using AppSync Visual editor or Amplify cli. Once that’s done, AppSync takes care of everything like enabling Dynamodb resources and creating resolver functions for our schema.

Getting Started with the Amplify Framework

First, we need to install the Amplify command-line tool which is used to create and maintain serverless backends on AWS.

Run the below command to install the aws-amplify.

npm install -g @aws-amplify/cli

Mac users need to use sudo before npm.

Once you have successfully installed it, you need to configure your AWS account by running the following command.

amplify configure

Watch this video to configure your cli with your Aws account.

Creating a React App

Use the create-react-app to create the react app.

npx create-react-app awsgraphql-react

The above command will download the required files in the “awsgraphql-react” folder to start the react app.

cd awsgraphql-react change the working directory.

Adding GraphQL Backend

Run the following command to initialize the new amplify project.

amplify init

It prompts with different questions like choosing your favorite code editor and type of app you are building.

Now open your project folder in your code editor you will see a amplify folder and .amplifyrc file is added to your react app.

Once you successfully initialized the amplify project Its time to add an AppSync graphql API to our project by running the following command.

amplify add api

This command will prompt with two options Rest or GraphQL choose GraphQL.

? Please select from one of the below-mentioned services (Use arrow keys)
❯ GraphQL
  REST

Name your GraphQL endpoint and choose authorization type Api key.

? Please select from one of the below mentioned services GraphQL
? Provide API name: awsgraphqlreact
? Choose an authorization type for the API (Use arrow keys)
❯ API key
  Amazon Cognito User Pool

Now you need to select the following options.

? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with
 ID, name, description)
? Do you want to edit the schema now? Yes

Let’s edit our schema before pushing it to the AWS open your graphql schema which is located in the following folder amplify/backend/api/awsgraphqlreact/schema.graphql.

Remove everything and add the schema below.

type Post @model {
    id: ID!
    title: String!
    body:String!
    createdAt:String!
}

This a Post object type with four fields ID,title,body and createdAt.

@model : This a model directive that tells amplify cli to store the following types in the dynamodb table.

Now run the below command to update your backend schema.

amplify push

This command will prompt with following questions and choose yes for every question.

| Category | Resource name   | Operation | Provider plugin   |
| -------- | --------------- | --------- | ----------------- |
| Api      | awsgraphqlreact | Create    | awscloudformation |
? Are you sure you want to continue? Yes

GraphQL schema compiled successfully. Edit your schema at /Users/saigowtham/Desktop/awsgraphql-react/amplify/backend/api/awsgraphqlreact/schema.graphql
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations
and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations
- queries,mutations and subscriptions Yes

If you open your AWS console https://console.aws.amazon.com/appsync/ you can see a complete schema file with queries , mutations and resolver functions which is created by the aws-amplify cli by using our Post object type.

Connecting GraphQL API to React

Now we are connecting our GraphQL backend with the react app for this first we need to download the following packages.

npm install aws-appsync graphql-tag react-apollo

Once you successfully installed, now open your index.js file in your react app and add the below configuration.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import gql from 'graphql-tag';
import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync';
import aws_config from './aws-exports';

const client = new AWSAppSyncClient({
    url: aws_config.aws_appsync_graphqlEndpoint,
    region: aws_config.aws_appsync_region,
    auth: {
        type: AUTH_TYPE.API_KEY,
        apiKey: aws_config.aws_appsync_apiKey,
    }
});

ReactDOM.render(<App />, document.getElementById('root'));

After that, we import the AWSAppSyncClient constructor, AUTH_TYPE from the aws-appsync package and aws_config from the ./aws-exports file which is created automatically by the amplify cli.

Next, we’ll have to instantiate the new AWSAppSyncClient client by passing the aws_config.

Running the first query

In graphql ‘query’ is used to fetch the data from the graphql endpoint.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import gql from 'graphql-tag';
import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync';
import aws_config from './aws-exports';

import { listPosts } from './graphql/queries';

const client = new AWSAppSyncClient({
    url: aws_config.aws_appsync_graphqlEndpoint,
    region: aws_config.aws_appsync_region,
    auth: {
        type: AUTH_TYPE.API_KEY,
        apiKey: aws_config.aws_appsync_apiKey,
    }
});

client.query({
    query: gql(listPosts)
}).then(({ data }) => {
    console.log(data);
});

ReactDOM.render(<App />, document.getElementById('root'));

In the code above, we invoke the client.query method by passing a listPosts query which is generated automatically by the aws-amplify based on our graphql endpoint.

You’ll find the data of this query logged inside your browser console.

Since we don’t have any data in our dynamodb table so that we got 0 items, which is what we should expect.

Let’s use the ‘react-apollo’ to run the queries and mutations from the UI.

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync';
import aws_config from './aws-exports';
import { ApolloProvider } from 'react-apollo'

const client = new AWSAppSyncClient({
    url: aws_config.aws_appsync_graphqlEndpoint,
    region: aws_config.aws_appsync_region,
    auth: {
        type: AUTH_TYPE.API_KEY,
        apiKey: aws_config.aws_appsync_apiKey,
    }
});



ReactDOM.render(<ApolloProvider client={client}>
    <App />
</ApolloProvider>, document.getElementById('root'));

Next we import an ApolloProvider component from the ‘react-apollo’ and wrap it in our App component by passing a client so that we can access the client anywhere from our react app.

Creating a Post

We need to create a new component called CreatePost in the createPost.js file which helps us to run the Mutation and add data to our backend.

createPost.js

import React from "react";
import { Mutation } from "react-apollo";
import { createPost } from "./graphql/mutations";
import gql from "graphql-tag";

class CreatePost extends React.Component {
  handleSubmit = (e, createPost) => {
    e.preventDefault();
    createPost({
      variables: {
        input: {
          title: this.title.value,
          body: this.body.value,
          createdAt: new Date().toISOString()
        }
      }
    }).then(res => {
      this.title.value = "";
      this.body.value = "";
    });
  };
  render() {
    return (
      <div>
        <h1>Create post</h1>

        <Mutation mutation={gql(createPost)}>
          {(createPost, { data, loading, error }) => {
            return (
              <div>
                <form
                  className="add-post"
                  onSubmit={e => this.handleSubmit(e, createPost)}
                >
                  <input
                    type="text" placeholder="Title"
                    ref={node => (this.title = node)}
                    required
                  />
                  <textarea
                    rows="3"
                    cols="40"
                    placeholder="Body"
                    ref={node => (this.body = node)}
                    required
                  />
                  <button>{loading ? "Yes boss..." : "Create Post"}
                  </button>
                </form>
                {error && <p>{error.message}</p>}
              </div>
            );
          }}
        </Mutation>
      </div>
    );
  }
}

export default CreatePost;

In CreatePost we have imported a Mutation component from the ‘react-apollo’ and gql from the ‘graphql-tag’. Then createPost mutation is imported from ./grahql/mutations file.

The ‘createPost’ mutation takes three dynamic arguments which are titlebodycreatedAt.

title: The title of our post.

body: The body of our post.

createdAt: Post creation time and date.

In your App.js import the createPost component.

App.js

import React, { Component } from 'react';
import CreatePost from './createPost';

class App extends Component {
  render() {
    return (
      <div className="App">
        <CreatePost />
      </div>
    );
  }
}

export default App;

Let’s test our createPost component by creating our first post.

Open your aws-console to see your data is stored inside the DynamoDB table.

Fetching the Data

Currently, we are not rendering any data on the UI so let’s query a data to GraphQL endpoint so that we can see newly created posts.

We’ll need to create two new components.

post.js

import React from 'react';

class Post extends React.Component {

    componentDidMount() {
        this.props.subscribeToMore();
    }


    render() {
        const items = this.props.data.listPosts.items;

        return items.map((post) => {
            return (
                <div>
                    <h1>{post.title}</h1>
                    <p>{post.body}</p>
                    <time dateTime={post.createdAt}>
                    {new Date(post.createdAt).toDateString()}</time>
                    <br />
                </div>

            )
        })


    }

}


export default Post;

displayPosts.js

import React from 'react'
import { Query } from 'react-apollo'
import { listPosts } from './graphql/queries';
import { onCreatePost } from './graphql/subscriptions'
import gql from 'graphql-tag';
import Post from './post'

class DisplayPosts extends React.Component {

    subsCribeNewPosts = (subscribeToMore) => {
        return subscribeToMore({
            document: gql(onCreatePost),
            updateQuery: (prev, { subscriptionData }) => {
                if (!subscriptionData.data) return prev;
                const newPostData = subscriptionData.data.onCreatePost;
                return Object.assign({}, prev, {
                    listPosts: {
                        ...prev.listPosts,
                        items: [...prev.listPosts.items, newPostData]
                    }
                })
            }
        })
    }


    render() {
        return (
            <div className="posts">
                <Query query={gql(listPosts)}  >
                    {({ loading, data, error, subscribeToMore }) => {

                        if (loading) return <p>loading...</p>
                        if (error) return <p>{error.message}</p>

                        return <Post data={data} subscribeToMore={() =>
                            this.subsCribeNewPosts(subscribeToMore)} />
                    }}
                </Query>



            </div>
        )
    }
}


export default DisplayPosts;

In the DisplayPosts component, we query the list of posts and also enable real time subscriptions so that we can see newly created posts rendered first.

Inside the Query component, we access the subscribeToMore function and pass it to the subscribeNewPosts method.

subscribeToMore: it is invoked whenever the Post component is mounted to the dom and listen for the new posts added to our graphql API.

updateQuery: the updateQuery function is used to merge the previous data and current data.

Update your App.js file by importing the DisplayPostscomponent.

App.js

import React, { Component } from 'react';
import CreatePost from './createPost';
import DisplayPosts from './displayPosts';

class App extends Component {
  render() {
    return (
      <div className="App">
        <CreatePost />
        <DisplayPosts />
      </div>
    );
  }
}

export default App;

Let’s test our DisplayPosts component by creating new posts.

In the above image, we tested it by opening two new browser windows.

Edit Post

Let’s create the EditPost component which helps us to edit the previously created post.

editPost.js

import React from "react";
import { updatePost } from "./graphql/mutations";
import { Mutation } from "react-apollo";
import gql from "graphql-tag";

class EditPost extends React.Component {
  state = {
    show: false,
    postData: {
      title: this.props.title,
      body: this.props.body
    }
  };

  handleModal = () => {
    this.setState({ show: !this.state.show });
    document.body.scrollTop = 0;
    document.documentElement.scrollTop = 0;
  };

  handleSubmit = (e, updatePost) => {
    e.preventDefault();
    updatePost({
      variables: {
        input: {
          id: this.props.id,
          title: this.state.postData.title,
          body: this.state.postData.body
        }
      }
    }).then(res => this.handleModal());
  };

  handleTitle = e => {
    this.setState({
      postData: { ...this.state.postData, title: e.target.value }
    });
  };

  handleBody = e => {
    this.setState({
      postData: { ...this.state.postData, body: e.target.value }
    });
  };

  render() {
    return (
      <>
        {this.state.show && (
          <div className="modal">
            <button className="close" onClick={this.handleModal}>
              X
            </button>
            <Mutation mutation={gql(updatePost)}>
              {updatePost => {
                return (
                  <form
                    className="add-post"
                    onSubmit={e => this.handleSubmit(e, updatePost)}
                  >
                    <input
                      type="text"
                      required
                      value={this.state.postData.title}
                      onChange={this.handleTitle}
                    />
                    <textarea
                      rows="3"
                      cols="40"
                      required
                      value={this.state.postData.body}
                      onChange={this.handleBody}
                    />
                    <button>Update Post</button>
                  </form>
                );
              }}
            </Mutation>
          </div>
        )}
        <button onClick={this.handleModal}>Edit</button>
      </>
    );
  }
}

export default EditPost;

In EditPost we are going to import the Mutation component,updatePost mutation and gql tag then we use the Mutation component by passing the mutation prop.

In the Mutation component, we need to pass the function as children because it is using the render props pattern.

The first parameter of the function is the mutation function so that we passed this function as an argument to the handleSubmit method and invoked with the updated post title and body.

Open your post.js file and add the EditPost component.

post.js

import React from 'react';
import EditPost from './editPost'

class Post extends React.Component {

    componentDidMount() {
        this.props.subscribeToMore();
    }


    render() {
        const items = this.props.data.listPosts.items;

        return items.map((post) => {
            return (
                <div>
                    <h1>{post.title}</h1>
                    <p>{post.body}</p>
                    <time dateTime={post.createdAt}>
                    {new Date(post.createdAt).toDateString()}</time>
                    <br />
                    <EditPost {...post} />
                </div>

            )
        })


    }

}

export default Post;

Let’s test our EditPost component by editing any previously created post.

DeletePost

Now we are implementing DeletePost component with Optimistic UI.

What is Optimistic UI?

For example, if we delete a Post, it takes time to get the response from the server, and only then can we update the UI. With Optimistic UI we can render this component and once we got a response from the server we replace the optimistic result with actual server result.

Create a new file called deletePost.js.

deletePost.js

import React, { Component } from 'react'
import { Mutation } from 'react-apollo';
import { deletePost } from './graphql/mutations';
import gql from 'graphql-tag';
import { listPosts } from './graphql/queries';


class DeletePost extends Component {

    handleDelete = (deletePost) => {
        deletePost({
            variables: {
                input: {
                    id: this.props.id
                }
            },
            optimisticResponse: () => ({
                deletePost: {
                    // This type must match the return type of
                    //the query below (listPosts)
                    __typename: 'ModelPostConnection',
                    id: this.props.id,
                    title: this.props.title,
                    body: this.props.body,
                    createdAt: this.props.createdAt
                }
            }),
            update: (cache, { data: { deletePost } }) => {
                const query = gql(listPosts);

                // Read query from cache
                const data = cache.readQuery({ query });

                // Add updated postsList to the cache copy
                data.listPosts.items = [
                    ...data.listPosts.items.filter(item =>
                     item.id !== this.props.id)
                ];

                //Overwrite the cache with the new results
                cache.writeQuery({ query, data });
            }
        })
    }

    render() {
        return (
            <Mutation mutation={gql(deletePost)}>
                {(deletePost, { loading, error }) => {
                    return <button onClick={
                       () => this.handleDelete(deletePost)}>
                        Delete Post</button>
                }}
            </Mutation>
        )
    }
}


export default DeletePost;

In optimisticResponse function we passed exactly the delete Post data with __typename:'ModelPostConnection' then we update the cache by removing the deleted post.

Update your post.js file by adding DeletePost component.

post.js

import React from 'react';
import EditPost from './editPost'
import DeletePost from './deletePost'

class Post extends React.Component {

    componentDidMount() {
        this.props.subscribeToMore();
    }

    render() {
        const items = this.props.data.listPosts.items;

        return items.map((post) => {
            return (
                <div key={post.id}>
                    <h1>{post.title}</h1>
                    <p>{post.body}</p>
                    <time dateTime={post.createdAt}>{
                        new Date(post.createdAt).toDateString()}</time>
                    <br />
                    <EditPost {...post} />
                    <DeletePost {...post} />
                </div>

            )
        })
    }
}

export default Post;

In the above, we have tested it in offline mode but we can see the UI is updated instantly through an “optimistic response” once we got online appsync send a deletePost mutation to update our backend.

Hosting the React app

By using amplify-cli we can also host our react app in Aws s3 bucket and CloudFront.

Open your terminal and run the following command.

amplify hosting add

Code repository

For monitoring, debugging and error detection of AWS Lambdas we are using Dashbird.

Why Dashbird?

  • Dashbird helps us proactively monitoring health and errors.
  • One main thing about Dashbird is its user-friendly easy to understand interface.
  • Dashbird visualizes all your AWS Lambda metrics like memory utilization, invocation count, and execution duration.

Read more about debugging serverless applications with Dashbird.

Dashbird Interface

And this is it, thank you for sticking with me until the very end. I know, this was an extremely long post and I have to congratulate you for sticking with it.

Read our blog

ANNOUNCEMENT: new pricing and the end of free tier

Today we are announcing a new, updated pricing model and the end of free tier for Dashbird.

4 Tips for AWS Lambda Performance Optimization

In this article, we’re covering 4 tips for AWS Lambda optimization for production. Covering error handling, memory provisioning, monitoring, performance, and more.

AWS Lambda Free Tier: Where Are The Limits?

In this article we’ll go through the ins and outs of AWS Lambda pricing model, how it works, what additional charges you might be looking at and what’s in the fine print.

Made by developers for developers

Dashbird was born out of our own need for an enhanced serverless debugging and monitoring tool, and we take pride in being developers.

What our customers say

Dashbird gives us a simple and easy to use tool to have peace of mind and know that all of our Serverless functions are running correctly. We are instantly aware now if there’s a problem. We love the fact that we have enough information in the Slack notification itself to take appropriate action immediately and know exactly where the issue occurred.

Thanks to Dashbird the time to discover the occurrence of an issue reduced from 2-4 hours to a matter of seconds or minutes. It also means that hundreds of dollars are saved every month.

Great onboarding: it takes just a couple of minutes to connect an AWS account to an organization in Dashbird. The UI is clean and gives a good overview of what is happening with the Lambdas and API Gateways in the account.

I mean, it is just extremely time-saving. It’s so efficient! I don’t think it’s an exaggeration or dramatic to say that Dashbird has been a lifesaver for us.

Dashbird provides an easier interface to monitor and debug problems with our Lambdas. Relevant logs are simple to find and view. Dashbird’s support has been good, and they take product suggestions with grace.

Great UI. Easy to navigate through CloudWatch logs. Simple setup.

Dashbird helped us refine the size of our Lambdas, resulting in significantly reduced costs. We have Dashbird alert us in seconds via email when any of our functions behaves abnormally. Their app immediately makes the cause and severity of errors obvious.