In this post, I will try to explain the steps needed to develop a production-ready serverless backend API powered by AWS AppSync and GraphQL that scales to MILLIONS of requests. And as a bonus, you will also learn how to develop Real-Time Subscriptions with AWS AppSync for real-time scalable updates for your web applications
Components Used
This gist uses the following frameworks or libraries:
- GraphQL
- React
- Node.js and NPM
This gist uses the following AWS services:
- AWS AppSync (For GraphQL)
- Amplify (For backend)
- DynamoDB (For Database)
- Amazon Cognito (For Authentication)
Steps
Step 1 :
To install aws-amplify
cli tools:
npm install -g @aws-amplify/cli
Step 2 :
To create a React App:
npx create-react-app AppName
Step 3 :
To start the react dev server on localhost:
npm start
Step 4 :
To initialize the AWS Amplify within the project:
amplify init
Then prompts will appear for necessary information about the project:
- App Name
- Environment Name
- IDE
- Framework
- language
- src folder
- Aws-Profile-> Yes
- select profile (Default)
Step 5 :
To add AWS Appsync:
amplify add api
Then prompts will appear for necessary information:
- GraphQL
- API Key or User Pool
- have schema -> No
- need Guided schema -> Yes
- single object with fields (for starter)
- create schema now -> Yes
- enter to continue
Step 6 :
To push these settings to AWS cloud:
amplify push
Then prompts will appear:
- continue -> Yes
- generate code -> Yes
- js
- generate all -> Yes
- max depth -> 2 is fine
Step 7 :
Go to Appsync console from command:
amplify console api
Step 8 :
To install AWS modules for working with react:
npm install aws-amplify aws-amplify-react
Step 9 :
To configure AWS in the react app:
In the index.js
file in the src
folder of React App:
import Amplify from 'aws-amplify'
import aws_exports from './aws-exports'
Amplify.configure(aws_exports)
Usage
Step 10 :
To use GraphQL queries within react:
import { <queryName> } from '../graphql/queries'
import { API, graphqlOperation } from 'aws-amplify'
class ComponentName extends Component {
componentDidMount = async () => {
this.getPosts()
}
getPosts = async () => {
const result = await API.graphql(graphqlOperation(listPosts))
this.setState({ posts: result.data.listPosts.items})
//console.log("All Posts: ", JSON.stringify(result.data.listPosts.items))
//console.log("All Posts: ", result.data.listPosts.items)
}
}
Step 11 :
To add Subscriptions to automatically listen to changes:
componentDidMount = async () => {
this.getPosts()
this.createPostListener = API.graphql(graphqlOperation(onCreatePost))
.subscribe({
next: postData => {
const newPost = postData.value.data.onCreatePost
const prevPosts = this.state.posts.filter( post => post.id !== newPost.id)
const updatedPosts = [newPost, ...prevPosts]
this.setState({ posts: updatedPosts})
}
})
componentWillUnmount() {
this.createPostListener.unsubscribe()
}
getPosts = async () => {
const result = await API.graphql(graphqlOperation(listPosts))
this.setState({ posts: result.data.listPosts.items})
//console.log("All Posts: ", JSON.stringify(result.data.listPosts.items))
//console.log("All Posts: ", result.data.listPosts.items)
}
Step 12 :
To add Authentication using Amazon Cognito:
amplify add auth
Prompts:
- Default configuration
- username/email
- No, I am done #### Step 13 :
amplify push
Step 14 :
in the App.js
file:
import { withAuthenticator } from 'aws-amplify-react'
export default withAuthenticator(App, { includeGreetings: true });
Step 15 :
componentDidMount = async () => {
this.getPosts()
await Auth.currentUserInfo()
.then(user => {
this.setState(
{
ownerId: user.attributes.sub,
ownerUsername: user.username,
}
)
})
}
Step 16 :
Complete example of CRUD of a blogpost with like, comment, edit, delete:
import React, { Component } from 'react'
import { listPosts } from '../graphql/queries'
import { API, graphqlOperation } from 'aws-amplify'
import DeletePost from './DeletePost'
import EditPost from './EditPost'
import { onCreatePost, onDeletePost, onUpdatePost, onCreateComment, onCreateLike } from '../graphql/subscriptions'
import { createLike } from '../graphql/mutations'
import CreateCommentPost from './CreateCommentPost'
import CommentPost from './CommentPost'
import { FaThumbsUp, FaSadTear } from 'react-icons/fa';
import {Auth} from 'aws-amplify'
import UsersWhoLikedPost from './UsersWhoLikedPost'
class DisplayPosts extends Component {
state = {
ownerId:"",
ownerUsername:"",
errorMessage: "",
postLikedBy: [],
isHovering: false,
posts: []
}
componentDidMount = async () => {
this.getPosts()
await Auth.currentUserInfo()
.then(user => {
this.setState(
{
ownerId: user.attributes.sub,
ownerUsername: user.username,
}
)
})
this.createPostListener = API.graphql(graphqlOperation(onCreatePost))
.subscribe({
next: postData => {
const newPost = postData.value.data.onCreatePost
const prevPosts = this.state.posts.filter( post => post.id !== newPost.id)
const updatedPosts = [newPost, ...prevPosts]
this.setState({ posts: updatedPosts})
}
})
this.deletePostListener = API.graphql(graphqlOperation(onDeletePost))
.subscribe({
next: postData => {
const deletedPost = postData.value.data.onDeletePost
const updatedPosts = this.state.posts.filter(post => post.id !== deletedPost.id)
this.setState({posts: updatedPosts})
}
})
this.updatePostListener = API.graphql(graphqlOperation(onUpdatePost))
.subscribe({
next: postData => {
const { posts } = this.state
const updatePost = postData.value.data.onUpdatePost
const index = posts.findIndex(post => post.id === updatePost.id) //had forgotten to say updatePost.id!
const updatePosts = [
...posts.slice(0, index),
updatePost,
...posts.slice(index + 1)
]
this.setState({ posts: updatePosts})
}
})
this.createPostCommentListener = API.graphql(graphqlOperation(onCreateComment))
.subscribe({
next: commentData => {
const createdComment = commentData.value.data.onCreateComment
let posts = [ ...this.state.posts]
for (let post of posts ) {
if ( createdComment.post.id === post.id) {
post.comments.items.push(createdComment)
}
}
this.setState({ posts})
}
})
this.createPostLikeListener = API.graphql(graphqlOperation(onCreateLike))
.subscribe({
next: postData => {
const createdLike = postData.value.data.onCreateLike
let posts = [...this.state.posts]
for (let post of posts ) {
if (createdLike.post.id === post.id) {
post.likes.items.push(createdLike)
}
}
this.setState({ posts })
}
})
}
componentWillUnmount() {
this.createPostListener.unsubscribe()
this.deletePostListener.unsubscribe()
this.updatePostListener.unsubscribe()
this.createPostCommentListener.unsubscribe()
this.createPostLikeListener.unsubscribe()
}
getPosts = async () => {
const result = await API.graphql(graphqlOperation(listPosts))
this.setState({ posts: result.data.listPosts.items})
//console.log("All Posts: ", JSON.stringify(result.data.listPosts.items))
//console.log("All Posts: ", result.data.listPosts.items)
}
likedPost = (postId) => {
for (let post of this.state.posts) {
if ( post.id === postId ) {
if ( post.postOwnerId === this.state.ownerId) return true;
for (let like of post.likes.items) {
if (like.likeOwnerId === this.state.ownerId) {
return true;
}
}
}
}
return false;
}
handleLike = async postId => {
if (this.likedPost(postId)) {return this.setState({errorMessage: "Can't Like Your Own Post."})} else {
const input = {
numberLikes: 1,
likeOwnerId: this.state.ownerId,
likeOwnerUsername: this.state.ownerUsername,
likePostId: postId
}
try {
const result = await API.graphql(graphqlOperation(createLike, { input }))
console.log("Liked: ", result.data);
}catch (error) {
console.error(error)
}
}
}
handleMouseHover = async postId => {
this.setState({isHovering: !this.state.isHovering})
let innerLikes = this.state.postLikedBy
for (let post of this.state.posts) {
if (post.id === postId) {
for ( let like of post.likes.items) {
innerLikes.push(like.likeOwnerUsername)
}
}
this.setState({postLikedBy: innerLikes})
}
console.log("Post liked by: ", this.state.postLikedBy);
}
handleMouseHoverLeave = async () => {
this.setState({isHovering: !this.state.isHovering})
this.setState({postLikedBy: []})
}
render() {
const { posts } = this.state
let loggedInUser = this.state.ownerId
return posts.map(( post ) => {
return (
<div className="posts" style={rowStyle} key={ post.id}>
<h1> { post.postTitle }</h1>
<p> { post.postBody }</p>
<br />
<span>
{post.postOwnerId === loggedInUser &&
<DeletePost data={post}/>
}
{ post.postOwnerId === loggedInUser &&
<EditPost {...post} />
}
<span>
<p className="alert">{ post.postOwnerId === loggedInUser && this.state.errorMessage}</p>
<p onMouseEnter={ () => this.handleMouseHover(post.id)}
onMouseLeave={ () => this.handleMouseHoverLeave()}
onClick={() => this.handleLike(post.id)}
style={{color: (post.likes.items.length > 0) ? "blue": "gray"}}
className="like-button">
<FaThumbsUp />
{post.likes.items.length}
</p>
{
this.state.isHovering &&
<div className="users-liked">
{this.state.postLikedBy.length === 0 ?
" Liked by No one " : "Liked by: " }
{this.state.postLikedBy.length === 0 ? <FaSadTear /> : <UsersWhoLikedPost data={this.state.postLikedBy} /> }
</div>
}
</span>
</span>
<span>
<CreateCommentPost postId={post.id} />
{ post.comments.items.length > 0 && <span style={{fontSize:"19px", color:"gray"}}>
Comments: </span>}
{
post.comments.items.map((comment, index) => <CommentPost key={index} commentData={comment}/>)
}
</span>
</div>
)
})
}
}
const rowStyle = {
background: '#f4f4f4',
padding: '10px',
border: '1px #ccc dotted',
margin: '14px'
}
export default DisplayPosts;
Step 17 :
To host the project in AWS S3:
amplify hosting add
Top comments (2)
{ post.comments.items.length > 0 &&
Comments: }
{
post.comments.items.map((comment, index) => )
}
Getting error from this: Uncaught (in promise) TypeError: Cannot read property 'length' of undefined
Getting error from this: Uncaught (in promise) TypeError: Cannot read property 'length' of undefined
from the DisplayPosts.js