With emerging cloud technologies, like Amplify and Azure functions, it is now easier than ever to build production ready, robust, and scalable modern web applications.

Developers can implement Authentication, APIs, data layers, machine learning, chatbots, even AR scenes more easily than ever by taking advantage of these new serverless cloud technologies.

In this practical guide I will walk you through building your very first full stack serverless application with React and AWS Amplify. I will walk you through some of the key concepts of AWS Amplify and React while building a sample application similar to reddit or hacker news.

We will cover the following topics.

  1. Configuring AWS Amplify 🛠️
  2. Adding a Serverless API to a React App 🧱
  3. User Authentication with Cognito  👩‍🚀
  4. GraphQL queries 🔍

The full source code for the completed project can be found at this link:

👉 Complete Code Here

Configuring AWS Amplify

Prerequisites:

Before continuing make sure you have the following configured.

  1. AWS account (Free Tier)
  2. Node.js 10.x or higher installed on your pc
  3. Npm 6.9.0. or higher
  4. Git installed on your pc

First of all we need to install the amplify cli. We can do this by running the following command:

npm install -g @aws-amplify/cli

After the installation is done we can configure amplify by running the following command:

amplify configure

It will ask you to sign in to AWS. Once you sign in it will prompt you with the instructions to create an IAM user.

***Note: IAM stands for (Identity Access Management). You can learn more about it in from this link. ***

You will see something similar to the below in your terminal.

Specify the AWS Region? 
region:  # Type Your preferred region (for me I choose ca-central)
Specify the username of the new IAM user:
user name:  # User name for Amplify IAM user
Complete the user creation using the AWS console (Y/n)

Select your preferred options. Amplify will the open up a browser tab and you have to login to your AWS console in the browser. Then it will ask you to create a user. Make sure you create the user with administrative privileges.

**Follow this short video example if you are not sure how to proceed:

View post on imgur.com

Once the user is created save the accessKeyId and the  secretAccessKey in a secure location. Amplify will ask you to provide these values in the terminal. When you see a user input prompt in the command line enter these values.

And that’s it. You are now all setup with Amplify.

Creating a new React app

Now that we are all setup with Amplify, let’s go ahead and create a new React application.

npx create-react-app serverless-blog
cd serverless-blog
npm start

Alright, we are now ready to add a serverless back-end to our React application.

Adding a Serverless API to React

Adding a serverless API is very simple with Amplify. All we have to do is run the following command and the amplify cli will walk us through the process of API creation.

In the root of our react app we have to run the following command:

amplify add api

Amplify CLI now will prompt us with some questions:

Please select from one of the below mentioned services: GraphQL # select GraphQL
Provide API name: serverlessblog  # choose a name for your api

Amplify gives us the options to choose REST or GraphQL. For this project we will be choosing GraphQL.

Moving into the next question it will ask you what type of authorization we would like.

Choose the default authorization type for the API

For this option choose Amazon Cognito User Pool. We will choose this option because in our app we want the users to have the ability to signup, login and logout. We will also only allow signed up users to create posts.

Next we will see a couple more questions. We can choose all the default options for these questions.

Do you want to use the default authentication and security configuration? # Select Default
How do you want users to be able to sign in? # Select Username
Do you want to configure advanced settings? # Select No
Do you want to configure advanced settings for the GraphQL API # Select No
Do you have an annotated GraphQL schema? # Select No
Do you want a guided schema creation? # Select Yes

Finally it will ask you how you would describe your project. For this option select one to many.

What best describes your project: One-to-many relationship 
(e.g., “Blogs” with “Posts” and “Comments”)

Then the API will be generated. We will see that a folder called Amplify has been generated. Locate the amplify/backend/api/schema.graphql file. This is our schema for the database. Let’s take a look inside of this file.

type Post @model @auth(
    rules: [
      {allow: owner, ownerField: "owner", operations: [create, update, delete]},
    ]
) {
  id: ID!
  title: String!
  content: String!
  owner: String
  comments: [Comment] @connection(keyName: "byPost", fields: ["id"])
}

type Comment @model @key(name: "byPost", fields: ["postID", "content"]) {
  id: ID!
  postID: ID!
  post: Post @connection(fields: ["postID"])
  content: String!
}

We have two models defined above, a Post model and a Comment model. A model represents a table in our database. By default we are using DynamoDB from Amazon.

You can login to AWS console in the browser and take a look at the DynamoDB tables. For our Post model we have mandatory fields id , title and content . The ! symbol represents a mandatory fields. Similarly Comment model also has a couple mandatory fields.

Now, the Post model has a @connection key defined in the comments field. This represents a foreign key join. The foreign key is joined to the id field of the Comment model by a key name byPost .

If we look at the Comment model we can see a @key key that is doing the same. This join is create a has_many relationship between two model. Therefore our posts has many comments. If you are familiar with relational databases you probably know this concept.

Notice that we have another keyword, @auth. This key ensures ownership. If we look at the rules associated to this key we will see that we are only allowing an owner (an authenticated user) to create, update and delete a record. It will also prevent a user from deleting a post created by some else.

Alright, now let’s push our code to AWS cloud with the following command:

Amplify push

Amplify cli will prompt you with an option to choose the code generation language. Choose Javascript. This will generate some code for us. We will be using this generated code for GraphQL queries and mutations.

You can find the code for the project up to this point at the following link:

👉 https://github.com/Shadid12/serverless-blog/tree/01-initial-setup

Let’s Create a Home Page 🏠

We would like to have a home page with all the latest posts listed. So let’s go to our src/App.js and create a new Home React component.

// src/App.js

import React from 'react';
import Home from './components/Home'

function App() {
  return (
    <Home />
  );
}

export default App;

We can create a new directory called component and create a new file Home.js for our Home component.

// src/components/Home.js

import React from 'react';

function Home() {
  const posts = [
      {
          id: 'id-1',
          title: 'dummy title',
          content: 'dummy content'
      },
      {
        id: 'id-2',
        title: 'dummy title 2',
        content: 'dummy content 2'
    }
  ];


  return (
    <div >
      <ul>
          {posts.map(p => (
              <li>
                  <h3><a href="">{p.title}</a></h3>
                  <p>{p.content}</p>
              </li>
          ))}
      </ul>
    </div>
  );
}

export default Home;

We created some hard coded values for posts. We will replace this with an API call later on.

User Authentication with Cognito

Let’s dive into authentication. First of all we need to install a couple npm packages.

npm i aws-amplify-react aws-amplify --save

These two packages make it really easy to add user authentication with React. First of all we need to configure the Amplify back end with React. Let’s open up src/index.js file and add the following code:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import Amplify from 'aws-amplify';
import aws_export from './aws-exports';

Amplify.configure(aws_export);

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

We are doing a couple things here. First of all we are importing aws_export. This file was generated through Amplify's code generator. This file contains our project metadata. Make sure to always gitignore this file as it contains sensitive data. We then import Amplify instance and call the configure method with aws_export as parameter.

Next we will go to our home component and we will import a higher order component called withAuthenticator from aws-amplify-react package. All we have to do it to wrap our Home component with this higher order component to make it authentication protected.

import React from 'react';
import { withAuthenticator } from 'aws-amplify-react';

function Home() {
  const posts = [
      {
          id: 'id-1',
          title: 'dummy title',
          content: 'dummy content'
      },
      {
        id: 'id-2',
        title: 'dummy title 2',
        content: 'dummy content 2'
    }
  ];


  return (
    <div >
      <ul>
          {posts.map(p => (
              <li>
                  <h3><a href="">{p.title}</a></h3>
                  <p>{p.content}</p>
              </li>
          ))}
      </ul>
    </div>
  );
}

export default withAuthenticator(Home );

That’s pretty much does the trick. Simple and easy. Now if we go to our app we should see a login screen. We are only able to view the Home component when we are logged in.

View post on imgur.com

Now that is neat ✨✨. Amplify even created an authentication verification process for us. First time users will receive an email confirmation to verify their account.

GraphQL queries

Classic CRUD

Pretty much every web application has the basic CRUD (Create, Read, Update, Delete) functionality. In our app we’ll have this functionality as well. A user will be able to create, read, update and delete posts and comments.

Creating a Post

When our user is logged in they should see a link to create a new post. When the user clicks the link it should take them to a new page with a form. Then the user submits the form and a new post is created.

To achieve this we need to be able to route to different links. We’ll be using the react-router-dom package to do the routing.

Let’s install this package and bring it in our project.

npm i react-router-dom --save

Now, in our App.js file we can modify our code to route into different urls.

import React from 'react';
import Home from './components/Home';
import Post from './components/Post';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

function App() {
  return (
    <Router>
      <div>
        <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/posts/new">New Post</Link>
            </li>
         </ul>

          <Switch>
            <Route exact path="/">
              <Home />
            </Route>
            <Route path="/posts/new">
              <Post />
            </Route>
          </Switch>
      </div>
    </Router>
  );
}

export default App;

As you can see in the code above we created a list of navigation links. The links corresponds to different React components. When we switch to a different link that component is mounted. The Home component is rendered in the root url and Post component will render in the /posts/new link.  We haven’t created the post component yet so let’s create that component.

import React from 'react';

function Post() {
    const [title, setTile] = React.useState();
    const [content, setContent] = React.useState();

    const handleSubmit = event => {
        event.preventDefault();
    }

    return (
      <div>
        <form onSubmit={handleSubmit}>
            <div>
            <label>
                Title:
                <input type="text" value={title} onChange={
                    (e) => setTile(e.target.value)
                } />
            </label>
            </div>
            <div>
            <label>
                Content:
                <textarea type="text" value={content} onChange={
                    (e) => setContent(e.target.value)
                } />
            </label>
            </div>
            <input type="submit" value="Submit" />
        </form>
      </div>
    );
}

export default Post;

This is a very basic React component where we have a form and we have two hooks that are changing the state of the component based on user input. When user submits the form we call the handleSubmit function. We will make our API call in this function. So let’s implement that.

If we navigate to the src/graphql/mutations.js we will see that amplify has already generated some code for us. In this file we have a function called createPost. We have to import this function in our component and execute the mutation. Here’s the code to do that:

import React from 'react';
import { API, graphqlOperation } from 'aws-amplify';
import { createPost } from '../graphql/mutations';

function Post() {
    const [title, setTitle] = React.useState();
    const [content, setContent] = React.useState();

    const handleSubmit = async event => {
        event.preventDefault();
        try {
             let input = {
               title,
               content
            }
             let newPost = await API.graphql(graphqlOperation(createPost, {input}));
             setTitle('');
             setContent('');
             console.log('new post created ->>', newPost)
            }catch (error) {
            console.log(error)
        }
    }

    return (
      <div>
        <form onSubmit={handleSubmit}>
            <div>
            <label>
                Title:
                <input type="text" value={title} onChange={
                    (e) => setTitle(e.target.value)
                } />
            </label>
            </div>
            <div>
            <label>
                Content:
                <textarea type="text" value={content} onChange={
                    (e) => setContent(e.target.value)
                } />
            </label>
            </div>
            <input type="submit" value="Submit" />
        </form>
      </div>
    );
}

export default Post;

You can login to AWS console in the browser and navigate to AppSync and select our application. Select Queries from the menu.

Picture of how to navigate to Queries in App Sync

From here click on docs and you will be able to see a detailed docs about all the queries and mutations for our app. We can also verify what parameters are needed for a generated mutation function.

View post on imgur.com

Listing all Posts

Let’s list all the posts in our home page now. If we go look at the src/graphql/queries.js file we will see a listPosts query function.  We can call this function to list all the posts. Let’s call this function in our Home component on component mount. This function will return all the posts from database and we will be rendering them in our home page.

// Home.js

import React from 'react';
import { withAuthenticator } from 'aws-amplify-react';
import { API, graphqlOperation } from 'aws-amplify';
import { listPosts } from '../graphql/queries';

function Home() {
  const [posts, setPosts] = React.useState([])
  React.useEffect(() => {
    getPosts();
  }, []);

  const getPosts = async () => {
    try {
      let resp = await API.graphql(graphqlOperation(listPosts));
      console.log('===>>>>',resp);
      setPosts(resp.data.listPosts.items)
    } catch (error) {
      console.log('Something went wrong', error);
    }
  }

  return (
    <div >
      <ul>
          {posts.map(p => (
              <li>
                  <h4><a href="">{p.title}</a></h4>
                  <span>By {p.owner}</span>
                  <div>
                    <button>delete</button>
                    <button>Edit</button>
                  </div>
              </li>
          ))}
      </ul>
    </div>
  );
}

export default withAuthenticator(Home);

The code up to this point can be found at the following link:

👉 https://github.com/Shadid12/serverless-blog/tree/03-create-read-update-delete

Deleting a Post

Let’s implement the ability to delete a post now. We are already rendering a delete button for each post. Now we will attach an action when the button is pressed.

As you can guess there is a GraphQL mutation for deleting a post. All we have to do is call this mutation. Here’s the code implementation

import React from 'react';
import { withAuthenticator } from 'aws-amplify-react';
import { API, graphqlOperation } from 'aws-amplify';
import { listPosts } from '../graphql/queries';
import { deletePost } from '../graphql/mutations';
import {
  Link
} from "react-router-dom";

function Home() {
  const [posts, setPosts] = React.useState([])
  React.useEffect(() => {
    getPosts();
  }, []);

  const getPosts = async () => {
    try {
      let resp = await API.graphql(graphqlOperation(listPosts));
      console.log('===>>>>',resp);
      setPosts(resp.data.listPosts.items)
    } catch (error) {
      console.log('Something went wrong', error);
    }
  }

  const deleteSelected = async id => {
    try {
      let input = {
        id
      }
      let deleted  = await API.graphql(graphqlOperation(deletePost, {input}));
      let newPosts = posts.filter(p => p.id !== id);
      setPosts(newPosts);
      console.log('Post delete', deleted);
    } catch (error) {
      alert('Not Allowed', error);
    }
  }


  return (
    <div >
      <ul>
          {posts.map(p => (
              <li>
                  <h4><a href="">{p.title}</a></h4>
                  <span>By {p.owner}</span>
                  <div>
                  <button onClick={() => deleteSelected(p.id)}>delete</button>
                  <Link to={`/posts/edit/${p.id}`}>Edit</Link>
                  </div>
              </li>
          ))}
      </ul>
    </div>
  );
}

export default withAuthenticator(Home);

As you can see above we pass in the postId as a param in the function and call the deletePost mutation.

Update a Post

For each post we have an update button. When a user clicks on this button we should route to an update post route. Let’s create a new route in our App Component.

// App.js

<Switch>
   <Route exact path="/">
      <Home />
   </Route>
   <Route path="/posts/new">
      <Post />
   </Route>
   <Route path="/posts/edit/:id">
      <EditPost />
   </Route>
</Switch>

We will create a new component called EditPost. This component will get the id of a post from the url and query that post in the database. After it retrieves a post it will let the user update its content. Once a user updates the input and hits the submit button it will make an update call to our API. Let’s implement this EditPost component.

import React from 'react';
import { API, graphqlOperation } from 'aws-amplify';
import { updatePost } from '../graphql/mutations';
import { getPost } from '../graphql/queries';
import { useParams } from "react-router-dom";

function EditPost() {
    let { id } = useParams();
    const [title, setTitle] = React.useState();
    const [content, setContent] = React.useState();

    React.useEffect(() => {
        queryPostById();
    }, [])

    const queryPostById = async () => {
        const resp = await API.graphql(graphqlOperation(getPost, { id }));
        console.log('--->>>>', resp)
        setTitle(resp.data.getPost.title);
        setContent(resp.data.getPost.content);
    }

    const handleSubmit = async event => {
        event.preventDefault();
        try {
            let input = {
                id,
                title,
                content
            }
            let newPost = await API.graphql(graphqlOperation(updatePost, {input}));
            setTitle('');
            setContent('');
            console.log('Post Updated ->>', newPost)
          } catch (error) {
            console.log(error)
        }
    }

    return (
      <div>
        <form onSubmit={handleSubmit}>
            <div>
            <label>
                Title:
                <input type="text" value={title} onChange={
                    (e) => setTitle(e.target.value)
                } />
            </label>
            </div>
            <div>
            <label>
                Content:
                <textarea type="text" value={content} onChange={
                    (e) => setContent(e.target.value)
                } />
            </label>
            </div>
            <input type="submit" value="Submit" />
        </form>
      </div>
    );
}

export default EditPost;

This component is very similar to our post component. The only main difference is that on component load we fetch a post and populate the input with the post information. A user can update the content and title. When a user submits the form we call the update mutation from graphql.

We have to change one last thing in our Home component. We have to add in change the edit link so that it routes to the proper post.

return (
    <div >
      <ul>
          {posts.map(p => (
              <li>
                  <h4><a href="">{p.title}</a></h4>
                  <span>By {p.owner}</span>
                  <div>
                  <button onClick={() => deleteSelected(p.id)}>delete</button>
                  <Link to={`/posts/edit/${p.id}`}>Edit</Link>
                  </div>
              </li>
          ))}
      </ul>
    </div>
  )

Perfect, we are done with the update functionality.

You can find the source code up to this point in the following repository:

👉https://github.com/Shadid12/serverless-blog/tree/04-delete-view

Viewing a Post

Let’s see how we can get a detailed view of a particular post. When a user clicks on the post it should route to a detailed view where the user can see the post title, detail and comments associated with that post.

This functionality is very similar to the update functionality. Let’s start with creating a new route in the App component.

<Route path="/posts/:id">
    <ViewPost />
</Route>

Now let’s create the ViewPost component.

import React from 'react';
import { API, graphqlOperation } from 'aws-amplify';
import { getPost } from '../graphql/queries';
import { useParams } from "react-router-dom";

function ViewPost() {
    let { id } = useParams();
    const [post, setPost] = React.useState();

    React.useEffect(() => {
        queryPostById();
    }, [])

    const queryPostById = async () => {
        const resp = await API.graphql(graphqlOperation(getPost, { id }));
        console.log('--->>>>', resp)
        setPost(resp.data.getPost);
    }

    if(!post) {
        return <div>Loading....</div>
    }
    return (
      <div>
        <h1>{post.title}</h1>
        <div>
            <span>By <b>{post.owner}</b></span>
        </div>
        <p>{post.content}</p>
        <ul>
            {
                post.comments.items.map(com => (<li>{com.content}</li>))
            }
        </ul>
      </div>
    );
}

export default ViewPost;

As you can see above code, the view post component is querying the post by id from database and then it is displaying the content of the post.

Now in our home component we need to change the url like below:

return (
    <div >
      <ul>
          {posts.map(p => (
              <li>
                  <h4><Link to={`/posts/${p.id}`}>{p.title}</Link></h4>
                  <span>By {p.owner}</span>
                  <div>
                  <button onClick={() => deleteSelected(p.id)}>delete</button>
                  <Link to={`/posts/edit/${p.id}`}>Edit</Link>
                  </div>
              </li>
          ))}
      </ul>
    </div>
  );

And that’s it. We have implemented create, read and update and delete functionality. As an exercise you can try to add comments to each posts.

I hope the topics discussed in this article were helpful. The basic concepts we discussed should be enough to get you started with your own full stack serverless applications.

If you have any questions feel free to get in touch with me. Until next time 😄