Monitoring platform for keeping systems up and running at all times.
Full stack visibility across the entire stack.
Detect and resolve any incident in record time.
Conform to industry best practices.
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.
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.
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.
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.
aws-amplify
npm install -g @aws-amplify/cli
Mac users need to use sudo before npm.
sudo
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.
Use the create-react-app to create the react app.
create-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.
cd awsgraphql-react
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.
amplify
.amplifyrc
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.
Rest
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.
Api
? 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.
Post
ID
title
body
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.
yes
| 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.
queries
mutations
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.
index.js
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.
AWSAppSyncClient
AUTH_TYPE
aws-appsync
aws_config
./aws-exports
Next, we’ll have to instantiate the new AWSAppSyncClient client by passing the aws_config.
In graphql ‘query’ is used to fetch the data from the graphql endpoint.
graphql
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.
listPosts
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.
0
Let’s use the ‘react-apollo’ to run the queries and mutations from the UI.
UI
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.
ApolloProvider
App
client
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
createPost.js
Mutation
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.
gql
createPost
./grahql/mutations
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.
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.
DisplayPosts
Inside the Query component, we access the subscribeToMore function and pass it to the subscribeNewPosts method.
subscribeToMore
subscribeNewPosts
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.
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.
Let’s create the EditPost component which helps us to edit the previously created post.
EditPost
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.
updatePost
mutation
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.
handleSubmit
Open your post.js file and add the EditPost component.
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.
Now we are implementing DeletePost component with Optimistic UI.
DeletePost
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.
optimisticResponse
__typename:'ModelPostConnection'
Update your post.js file by adding DeletePost component.
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.
deletePost
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.
Read more about debugging serverless applications with Dashbird.
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.
In this guide, we’ll talk about common problems developers face with serverless applications on AWS and share some practical strategies to help you monitor and manage your applications more effectively.
Today we are announcing a new, updated pricing model and the end of free tier for Dashbird.
In this article, we’re covering 4 tips for AWS Lambda optimization for production. Covering error handling, memory provisioning, monitoring, performance, and more.
Dashbird was born out of our own need for an enhanced serverless debugging and monitoring tool, and we take pride in being developers.
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.