January 16, 2022

Starting Full-stack Serveless Application Development with aws amplify - Part 1

Serveless Technologies like AWS Amplify allows us to focus entirely on business logic by providing Functions as a service (Faas) infrastructure to write our business logic as well as managed services for variety of common tasks such as authentication, databases, APIs and more .

In this tutorial, we will learn how to perform rapid application development by building a product Listing application called buymine with features like authentication as well as uploading and viewing products . We will use the AWS Amplify framework to achieve all that .

You can preview final product in the below url

https://dev.d3k3pym0e8rako.amplifyapp.com/

At glance, The AWS Amplify combine four things: -

  1. Amplify CLI - Allows you to create, manage, and remove AWS services directly from your terminal.
  2. Amplify Libraries - allow you to interact with AWS services from a web or mobile application by providing use-case centric, opinionated, declarative, and easy-to-use APIs.
  3. Amplify UI Components - is an open-source UI toolkit that encapsulates cloud-connected workflows inside of cross-framework UI components. It supports React, React Native, Angular, Web Components and Vue.
  4. Web Hosting Service - Allow deploy your application to a live domain complete with atomic deployments, continuous integration (CI), continuous deployment (CD), custom domains, and more.

Amplify CLI

We will build our application in the cloud directly from our front-end environment by utilizing Amplify CLI tool . Let’s go and install it if you don’t have it yet.

npm install -g @aws-amplify/cli

Note : Because we’re installing the Amplify CLI globally, you might need to run the command above with sudo depending on your system policies.

Once amplify has been installed globally, let’s then configure it to connect with our existing IAM (Identity and Access Management) user in your aws account by running below command : -

amplify configure

The command above, will prompt you to sign in to yor existing aws administrator account by opening a new tab on your browser . Go ahead sign in and once done, get back to the terminal and press enter to continue .

From there , you should be prompted to choose a region where your services will be hosted, in my case i choose us-east-1 . Once done click enter to proceed .

From there, you will be prompted to specify the username of the new IAM user. The name will be a local reference of the user that will be created in your AWS account . in my case i choose buymine-admin

Once you choose your username and press enter, the cli will open the browser specifically AWS IAM Dashboard preconfigured with the username we just specified . Go ahead and finish the process it should be straigh forward. In my case i went with all default options .

Once Done, IAM user will be created and you will be given a screen as below where you will be presented with your IAM user credentials namely Access Key ID and Secret Access key . Were only given secret access key once, so if you wanna reference it later, you can go ahead and download the CSV File.

AWS AIM User

Now back to our command line, click enter and you will prompted to enter accessKeyId and secretAccessKey, go ahead and paste them .

From there we would be asked if we want to create or update our aws profile on our local machine, in my case i left the profile name as default .

Creat react app

Now, the amplify cli has been configured succesfully and from here, we can start initiating building our cool app.

Let’s start by boostraping our react application using create-react-app .

npx create-react-app buymine

Once done, go ahead and cd to our project directory

cd buymine

From here, let’s start our app by running

npm start

Fro there, our app will launch in default port 3000 at http://localhost:3000/ .

Let’s add amplify

Now let’s install aws-amplify library that will contain all of the client-side APIs for interacting with the various AWS services we will be working with :-

npm install aws-amplify

Once installed, let’s initialize amplify project by running below command inside our project folder:-

amplify init

Fill in the requested inputs, choosing all defaults should be fine ; -

The init command once finished, it will create an amplify directory in our project directory. The amplify folder will store all our backend code for our project .

Any feature that will be added via cli, it’s code will also be added inside amplify directory. The init command will as well create aws-exports.js file that will contain configurations file.

Also if you now go to aws amplify dashboard, you will see a project already have been created fot you . You can run the command below to quickly go to the console .

amplify console

Now, we are good to go . Before we start adding aws services, let’s check some other important amplify commands that can be handy going forward : -

amplify status will show you what you’ve added already and if it’s locally configured or deployed

amplify add will allow you to add features like user login or a backend API

amplify push will build all your local backend resources and provision it in the cloud

amplify console to open the Amplify Console and view your project status

amplify publish will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

Building UI

Let’s start building our UI . Delete everything inside App.js file and replace with below ;

const App = () => {
  return (
    <div className="App">
      <h1>Homepage</h1>
    </div>
  );
};

export default App;

Note : as of react 17, it’s not require to import React . I have switch to using arrow function, it’s my personal preference and i find it to be more cleaner.

Inside index.js, let us import and load the amplify configuration file:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import Amplify from "aws-amplify";
import awsconfig from "./aws-exports";
Amplify.configure(awsconfig);

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

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

So far we have a white page with Homepage text. Let’s start adding some meat to our app. Let’s create a Header component that will have our app login button, logo, search and more.

We will be using material ui components and icons, so go ahead and install beforehand.

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material

Now let’s go ahead and create a components folder inside src directory, and inside it Header folder with two files Header.css and Header.js as below

Header.css

.header {
  display: flex;
  width: 100%;
  background: #f7bf50;
  justify-content: space-between;
  align-items: center;
}
.header img.header__logo {
  width: 250px;
  object-fit: contain;
  cursor: pointer;
}
.header__search {
  display: flex;
  flex: 1;
}
.header_searchInput {
  width: 95%;
}

.header__searchIcon {
  background: black;
  color: white;
  padding: 5px;
}
.header__menu {
  display: flex;
}
.header__menuItem {
  display: flex;
  align-items: center;
  padding: 0px 10px;
  cursor: pointer;
}
.header__menuItemText {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.header__menuItemAvatar {
  font-size: 34px;
  color: #333;
}
.header__menuItemText {
  font-size: 14px;
}
.header__menuItemText span {
  color: #333;
  font-size: 12px;
}

Header.js

import { useState } from "react";
import SearchIcon from "@mui/icons-material/Search";
import PersonIcon from "@mui/icons-material/Person";
import Modal from "@mui/material/Modal";
import Box from "@mui/material/Box";
import "./Header.css";

const modalStyle = {
  position: "absolute",
  top: "50%",
  left: "50%",
  transform: "translate(-50%, -50%)",
  width: 500,
  bgcolor: "background.paper",
  border: "2px solid #000",
  boxShadow: 24,
  p: 4,
};

const AppModal = ({ isOpen, onClose, component }) => {
  return (
    <Modal
      open={isOpen}
      onClose={onClose}
      aria-labelledby="modal-modal-title"
      aria-describedby="modal-modal-description"
    >
      <Box sx={modalStyle}>{component}</Box>
    </Modal>
  );
};

const Header = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  return (
    <div>
      <div className="header">
        <img
          src="https://codecotzbucket.s3.us-east-2.amazonaws.com/logo.png"
          alt="BuyMine Logo"
          className="header__logo"
        />
        <div className="header__search">
          <input type="text" className="header_searchInput" />
          <SearchIcon className="header__searchIcon" />
        </div>

        <div className="header__menu">
          <div
            className="header__menuItem"
            onClick={() => setIsModalOpen(true)}
          >
            <PersonIcon className="header__menuItemAvatar" />
            <div className="header__menuItemText">
              <span> Sign In</span>
              Account
            </div>
          </div>
        </div>
      </div>
      <AppModal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
        component={<h1>Auth Component </h1>}
      />
    </div>
  );
};

export default Header;

Go ahead to our App.js file , and replace the h1 with our newly created Header component .

import Header from "./components/Header/Header";
const App = () => {
  return (
    <div className="App">
      <Header />
    </div>
  );
};

export default App;

Now , if we open our app we should have our page with header . And when we click Sign In menu item , a modal should pop up with a message Auth Component .

Let’s Authenticate

We will begin identifying our users by ading authentication that will allow them to register , login , logout , manage password and all identity management tasks .

To do so, we will be using Amazon Cognito, a fully managed identity service from aws that lets you add user registration, authentication, account recovery and more to your web and mobile apps quickly and easily.

Amazon Cognito scales to millions of users and supports sign-in with social identity providers, such as Facebook, Google, and Amazon.

With Cognito you pay based on your monthly active users (MAUs) and so far it is free for the first 50,000 users of any app.

Adding it to our app is as easy as running a command below ;-

amplify add auth

Follow the prompts as below .

 ? Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections.
 How do you want users to be able to sign in? Email
 ? Do you want to configure advanced settings? Yes, I want to make some additional changes.
 Warning: you will not be able to edit these selections.
 ? What attributes are required for signing up? Address (This attribute is not supported by Facebook, Google, Login With Amazon, Signinwithapple.), Email, N
ame, Phone Number (This attribute is not supported by Facebook, Login With Amazon, Signinwithapple.)
 Do you want to enable any of the following capabilities?  N

Note that we choose advance settings so that we can add Name, Address and Phone Number attribute during registrations .

Once this is done, run push command to deploy our added service to the cloud

amplify push

Let’s go on and add our Auth Component . Inside components folder , create Auth folder and inside it , Auth.js file as below :-

const Auth = () => {
  return <h1>Real Auth Component</h1>;
};
export default Auth;

Now in our AppModal inside Header.js file , replace the h1 with our newly created Auth Component:-

import Auth from "../Auth/Auth";

<AppModal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
        component={<Auth />}
      />

The next task will be to add amplify auth flow to our Auth component . We can add authentication in two ways :-

  1. Use pre-built UI components - amplify comes with pre-built UI components for React,React Native, Angular and vue that will allow us to scaffold the entire authentication flows in just a couple of lines of code
  2. Call Authentication APIs manually - The Amplify client library exposes an Auth class that has over 30 methods that allow you full control over all aspects of the user authentication flow. Some examples of the methods available are Auth.signUp, Auth.signIn, and Auth.signOut

In our case, we will go with option 1 since it’s more quicker and gives us what we want with less code . Beacause we are on react, go ahead and install the amplify ui library for react

npm install @aws-amplify/ui-react

Now, let’s jump in to the magical stuff. To add authentication screens , we can use withAuthenticator higher-order component (HoC) or Authenticator Component .

In our case, we are going to use Authenticator component since it’s highly customizable .

Go ahead and replace the code under Auth.js component with below:-

import { Authenticator } from "@aws-amplify/ui-react";
import Button from "@mui/material/Button";
import "@aws-amplify/ui-react/styles.css";
const Auth = () => {
  return (
    <Authenticator>
      {({ signOut, user }) => (
        <main>
          <h1>Profile</h1>
          <p>Name : {user.attributes.name}</p>
          <p>Email : {user.attributes.email}</p>
          <p>Phone Number : {user.attributes.phone_number}</p>
          <p>Address : {user.attributes.address}</p>
          <Button variant="outlined" onClick={signOut}>
            Sign out
          </Button>
        </main>
      )}
    </Authenticator>
  );
};

export default Auth;

Now , clicking Sign In should render our authentication screen .

Let’s add a bit of styling by reducing the width of our Auth screen as well as give the button the color of our app. Go on and add Auth.css file inside our Auth component with below content :-

[data-amplify-authenticator][data-variation="modal"] {
  width: 50vw !important;
  height: 90vh !important;
}

.amplify-button[data-variation="primary"] {
  background: #f7bf50 !important;
  color: rgb(48, 64, 80) !important;
}

Go ahead , and import our Auth.css inside our Auth component .

From here , when you click sign in, we should have a well represented authentication screen as below:-

Authenticator  Login Screen

Now if you go ahead and try to create an account , you will notice that address field is missing even though we added it as we were adding auth . To add address to our signup we will have to modify Authenticator by adding custom sign up field prop

The Authenticator automatically renders most Cognito User Pools attributes, with the exception of address, gender, locale, picture, updated_at, and zoneinfo. Because these are often app-specific, they can be customized via Sign Up fields.

Let’s add custom sign up field, go ahead and change our Auth.js file as below:-

import React from "react";
import {
  Authenticator,
  TextField,
  useAuthenticator,
} from "@aws-amplify/ui-react";
import Button from "@mui/material/Button";
import "@aws-amplify/ui-react/styles.css";
import "./Auth.css";

const Auth = () => {
  return (
    <Authenticator
      signUpAttributes={["email", "phone_number", "name"]}
      components={{
        SignUp: {
          FormFields() {
            const { validationErrors } = useAuthenticator();

            return (
              <>
                {/* Re-use default `Authenticator.SignUp.FormFields` */}
                <Authenticator.SignUp.FormFields />

                {/* Append & require Address field to sign up  */}
                <TextField
                  errorMessage={validationErrors.address}
                  hasError={!!validationErrors.address}
                  name="address"
                  label="Address"
                  placeholder="Address"
                />
              </>
            );
          },
        },
      }}
      services={{
        async validateCustomSignUp(formData) {
          if (!formData.address) {
            return {
              acknowledgement: "You must enter address",
            };
          }
        },
      }}
    >
      {({ signOut, user }) => {
        console.log("User = ", user);
        return (
          <main>
            <h1>Profile</h1>
            <p>Name : {user.attributes.name}</p>
            <p>Email : {user.attributes.email}</p>
            <p>Phone Number : {user.attributes.phone_number}</p>
            <p>Address : {user.attributes.address}</p>
            <Button variant="outlined" onClick={signOut}>
              Sign out
            </Button>
          </main>
        );
      }}
    </Authenticator>
  );
};


export default Auth;

From here , now if you click sign up tab you should be able to see the address field as below:- Authenticator Login Screen with custom address field

Now that everything seems right , we might as well proceed and create an account.

Once the process is complete, you should be presented with the same sign up modal but this time around your profile details should show up . And that’s that for auth , you can go on and sign out and login just to test if everything works fine.

One more thing before we end our auth flow, notice that our menu button still says Sign In. Let’s go ahead and fix that.

we will use currentAuthenticatedUser() method of the Auth class to check if a user is logged in or not. Since we will need this logic in other places, let’s create it as a custom hook.

custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.

Go ahead in our project directory create a folder inside src directory called hooks and inside hooks Auth.js file.

import { useState, useEffect } from "react";
import { Auth } from "aws-amplify";

export const useAuthStatus = () => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [user, setUser] = useState({});
  useEffect(() => {
    Auth.currentAuthenticatedUser()
      .then((user) => {
        setIsAuthenticated(true);
        setUser(user.attributes);
      })
      .catch((err) => setIsAuthenticated(false));
  }, []);

  return { isAuthenticated, user };
};

Now let’s use the hook we just created in header.js file to check if the user is authenticated or not and display the appropriate message .

import { useState } from "react";
import SearchIcon from "@mui/icons-material/Search";
import PersonIcon from "@mui/icons-material/Person";
import Modal from "@mui/material/Modal";
import Box from "@mui/material/Box";
import Auth from "../Auth/Auth";
import { useAuthStatus } from "../../hooks/Auth";
import "./Header.css";

const modalStyle = {
  position: "absolute",
  top: "50%",
  left: "50%",
  transform: "translate(-50%, -50%)",
  width: 500,
  bgcolor: "background.paper",
  border: "2px solid #000",
  boxShadow: 24,
  p: 4,
};

const AppModal = ({ isOpen, onClose, component }) => {
  return (
    <Modal
      open={isOpen}
      onClose={onClose}
      aria-labelledby="modal-modal-title"
      aria-describedby="modal-modal-description"
    >
      <Box sx={modalStyle}>{component}</Box>
    </Modal>
  );
};

const Header = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const { user, isAuthenticated } = useAuthStatus();

  return (
    <div>
      <div className="header">
        <img
          src="https://codecotzbucket.s3.us-east-2.amazonaws.com/logo.png"
          alt="BuyMine Logo"
          className="header__logo"
        />
        <div className="header__search">
          <input type="text" className="header_searchInput" />
          <SearchIcon className="header__searchIcon" />
        </div>

        <div className="header__menu">
          <div
            className="header__menuItem"
            onClick={() => setIsModalOpen(true)}
          >
            <PersonIcon className="header__menuItemAvatar" />
            <div className="header__menuItemText">
              <span>{isAuthenticated ? "Hello" : "Sign In"}</span>
              {isAuthenticated ? user.name : "Account"}
            </div>
          </div>
        </div>
      </div>
      <AppModal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
        component={<Auth />}
      />
    </div>
  );
};

export default Header;

Now once logged in , you should see hello {name} in our menu and clicking that should open your profile modal .

That’s it for now, on the last part of this tutorial series, we will finalize adding APIs for creating product, displaying product and more. See you on the other side :-)


Robert Rutenge
Front-end Developer | ReactJS enthusiast | Problem Solver . Find me on twitter and Github