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 occurred while the device was offline and not the entire database.
How does AppSync Works?
We'll create our GraphQL schema by using AppSync Visual editor or Amplify cli. Once that's done, AppSync takes cares 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 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.
Create 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 follow the 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 which 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 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 title
, body
, createdAt
.
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 DisplayPosts
component.
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 following command.
amplify hosting add
For monitoring, debugging and error detection of AWS Lambdas we are using Dashbird.
Why Dashbird?
Dashbird helps us actively monitoring the health and errors.
One main thing about Dashbird is its user-friendly Interface.
Dashbird visualizes all your AWS Lambda metrics like
memory utilization, invocation count, and execution duration.
DashBird Interface
I know, this was an extremely long post and I have to congratulate you for sticking with it. Since you took the time to read all of it I'd love to hear your thoughts. Please leave a comment letting me know what you liked or disliked about it.
Mad props to Sai for creating such a massive and comprehensive tutorial. We look forwrd to reading his next one. Check out his website here.
I've had this originally posted on the Dashbird blog and since it was so popular there I figured you guys might like it too.
Top comments (2)
Can AWS Amplify be used totally offline? I'm making an electron app with a free tier that will only store data locally and a premium tier that will sync with a cloud service for back-up and other goodies. But can't find if amplify and appsync can be used totally offline.
As far as I know, you can't (please let me know if I'm wrong). I recommend you take a look at the Booster Framework: booster.cloud/. It simplifies a lot of the development time, and it's getting close to finishing the local provider. I hope this helps in your case!