DEV Community

Cover image for Going serverless with React and AWS Amplify Part 2: Creating And Using Serverless Services
Peter Mbanugo
Peter Mbanugo

Posted on • Originally published at pmbanugo.me

Going serverless with React and AWS Amplify Part 2: Creating And Using Serverless Services

Serverless is a cloud-computing execution model in which the cloud provider is responsible for executing a piece of code by dynamically allocating resources to run the code when needed. In a previous post, we looked at what serverless is, and we set up our computer to be able to build serverless applications using AWS Amplify. We bootstrapped a React project and added the Amplify library to it. In this post, we will use the Amplify CLI to provision a secured backend API and a NoSQL database. Then we will consume this API from the React project.

Creating The Serverless Backend Services

The application we're going to build will allow users to perform basic CRUD operation. We will use a REST API with a NoSQL database. Follow the instruction below to create the serverless backend.

  1. Open the command line and go to the root directory of your project.
  2. Run the command amplify add api.
  3. You get a prompt to select a service type. Choose REST and press Enter.
  4. It prompts you to enter a name for the current category (the api category). Enter todosApi and press Enter.
  5. You're asked for a path. Accept the default items path by pressing Enter.
  6. The next prompt asks for the Lambda source. The serverless REST API works by creating a path on API Gateway and mapping that path to a lambda function. The lambda function contains code to execute when a request is made to the path it's mapped to. We will create a new lambda. Select the option Create a new Lambda function and press Enter.
  7. Enter todosLambda as the name of the resource for the category (function category), and press Enter.
  8. You will be asked for the name of the lambda function. Enter todos and press Enter.
  9. You will be asked to choose a template for generating code for this function. Choose the option CRUD function for Amazon DynamoDB table (Integration with Amazon API Gateway and Amazon DynamoDB) and press Enter. This creates an architecture using API Gateway with Express running in an AWS Lambda function that reads and writes to Amazon DynamoDB.
  10. The next prompt asks you to choose a DynanoDB data source. We do not have an existing DynamoDB table so we will choose the Create a new DynamoDB table option. Press Enter to continue. Now you should see DynamoDB database wizard. It'll ask a series of questions to determine how to create the database.
  11. You will be asked to enter the name for this resource. Enter todosTable and press Enter.
  12. The next prompt is for the table name. Enter todos and press Enter.
  13. You will be asked to add columns to the DynamoDB table. Follow the prompt to create column id with String as its type.
  14. Select id column when asked for the partition key (primary key) for the table.
  15. You will be asked if you want to add a sort key to the table. Choose false.
  16. The next prompt asks if you want to add global secondary indexes to your table. Enter n and press Enter. You should see the message Successfully added DynamoDb table locally
  17. The next prompt asks Do you want to edit the local lambda function now?. Enter n and press Enter. You should see the message Successfully added the Lambda function locally.
  18. You get asked if you want to restrict access to the API. Enter y and press Enter.
  19. For the next prompt, choose Authenticated and Guest users and press Enter. This option gives both authorized and guest users access to the REST API.
  20. Next, you get asked What kind of access do you want for Authenticated users. Choose read/write and press Enter.
  21. Now we get a prompt to choose the kind of access for unauthenticated users (i.e gues users). Choose read and press Enter. You should get the message Successfully added auth resource locally. This is because we have chosen to restrict access to the API, and the CLI added the Auth category to the project since we don't have any for the project. At this point, we've added resources that are needed to create our API (API Gateway, DynamoDB, Lambda function, and Cognito for authentication).
  22. We get asked if we want to add another path to the API. Enter n and press Enter. This completes the process and we get the message Successfully added resource todosApi locally.

amplify-add-api.gif

The amplify add api command took us through the process of creating a REST API. This API will be created based on the options we chose. To create this API requires 4 AWS services. They're:

  1. Amazon DynamoDB. This will serve as our NoSQL database. We created a DynomoDB table named todos when we added the todosTable resource. We gave it 3 columns with id as the primary key.
  2. AWS Lambda functions. This lets us run code without provisioning or managing servers. This is where our code to perform CRUD operations on the DynamoDB table will be.
  3. Amazon Cognito. This is responsible for authentication and user management. This lets us add user sign-up, sign-in, and access control to our app. We chose the option to restrict access to our API, and this service will help us authenticate users.
  4. Amazon API Gateway. This is what allows us to create REST API endpoint. We added a resource for this named todosApi, with a path items. We also selected the option to restrict access to the API.

However, the service specifications for this services are not yet in the cloud. We need to update the project in the cloud with information to provision the needed services. Run the command amplify status, and we should get a table with information about the amplify project.

Category Resource name Operation Provider plugin
Storage todosTable Create awscloudformation
Function todosLambda Create awscloudformation
Auth cognitodc1bbadf Create awscloudformation
Api todosApi Create awscloudformation

It lists the category we added along with the resource name and operation needed to be run for that resource. What the Create operation means is that these resources need to be created in the cloud. The init command goes through a process to generate the .amplifyrc file (it is written to the root directory of the project) and inserts a amplify folder structure into the project’s root directory, with the initial project configuration information written in it. Open the amplify folder and you'll find folders named backend and #current-cloud-backend. The backend folder contains the latest local development of the backend resources specifications to be pushed to the cloud, while #current-cloud-backend contains the backend resources specifications in the cloud from the last time the push command was run. Each resources stores contents in its own subfolder inside this folder.

Open the file backend/function/todosLambda/src/app.js. You will notice that this file contains code generated during the resource set up process. It uses Express.js to set up routes, and aws-serverless-express package to easily build RESTful APIs using the Express.js framework on top of AWS Lambda and Amazon API Gateway. When we push the project configuration to the cloud, it'll configure a simple proxy API using Amazon API Gateway and integrate it with this Lambda function. The package includes middleware to easily get the event object Lambda receives from API Gateway. It was applied on line 32 app.use(awsServerlessExpressMiddleware.eventContext()); and used across the routes with codes that looks like req.apiGateway.event.*. The pre-defined routes allow us to perform CRUD operation on the DynamoDB table. We will make a couple of changes to this file. The first will be to change the value for tableName variable from todosTable to todos. When creating the DynamoDB resource, we specified todosTable as the resource name and todos as the table name, so it wrongly used the resource name as the table name when the file was created. This would likely be fixed in a future version of the CLI, so if you don't find it wrongly used, you can skip this step. We will also need to update the definitions.

Change the first route definition to use the code below.

app.get(path, function(req, res) {
  const queryParams = {
    TableName: tableName,
    ProjectionExpression: "id, title"
  };

  dynamodb.scan(queryParams, (err, data) => {
    if (err) {
      res.json({ error: "Could not load items: " + err });
    } else {
      res.json(data.Items);
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

This defines a route to respond to the /items path with code to return all data in the DynamoDB table. The ProjectionExpression values are used to specify that it should get only the columns id and title.

Change the route definition on line 77 to read as app.get(path + hashKeyPath + sortKeyPath, function(req, res) {. This allows us retrieve an item by its id following the path /items/:id. Also change line 173 to be app.delete(path + hashKeyPath + sortKeyPath, function(req, res) {. This respnds to HTTP DELETE method to delete an item following the path /items/:id.

The AWS resources have been added and updated locally, and we need to provision them in the cloud. Open the command line and run amplify push. You'll get a prompt if you want to continue executing the command. Enter y and press Enter. What this does is that it'll upload the latest versions of the resources nested stack templates to an S3 deployment bucket, and then call the AWS CloudFormation API to create / update resources in the cloud.

Building The Frontend

When the amplify push command completes, you'll see a file aws-exports.js in the src folder. This file contains information to the resources that were created in the cloud. Each time a resource is created or updated by running the push command, this file will be updated. It's created for JavaScript projects and will be used in Amplify JavaScript library. We will be using this in our React project. We will also use Bootstrap to style the page. Open public/index.html and add the following in the head:

<link
  rel="stylesheet"
  href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
  integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
  crossorigin="anonymous"
/>
<script
  src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
  integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
  crossorigin="anonymous"
></script>
<script
  src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
  integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
  crossorigin="anonymous"
></script>
<script
  src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
  integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
  crossorigin="anonymous"
></script>
Enter fullscreen mode Exit fullscreen mode

Add a new file src/List.js with the following content:

import React from "react";

export default props => (
  <div>
    <legend>List</legend>
    <div className="card" style={{ width: "25rem" }}>
      {renderListItem(props.list, props.loadDetailsPage)}
    </div>
  </div>
);

function renderListItem(list, loadDetailsPage) {
  const listItems = list.map(item => (
    <li
      key={item.id}
      className="list-group-item"
      onClick={() => loadDetailsPage(item.id)}
    >
      {item.title}
    </li>
  ));

  return <ul className="list-group list-group-flush">{listItems}</ul>;
}
Enter fullscreen mode Exit fullscreen mode

This component will render a list of items from the API. Add a new file src/Details.js with the following content:

import React from "react";

export default props => (
  <div>
    <h2>Details</h2>
    <div className="btn-group" role="group">
      <button
        type="button"
        className="btn btn-secondary"
        onClick={props.loadListPage}
      >
        Back to List
      </button>
      <button
        type="button"
        className="btn btn-danger"
        onClick={() => props.delete(props.item.id)}
      >
        Delete
      </button>
    </div>
    <legend>{props.item.title}</legend>
    <div className="card">
      <div className="card-body">{props.item.content}</div>
    </div>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

This component will display the details of an item with buttons to delete that item or go back to the list view. Open src/App.js and update it with this code:

import React, { Component } from "react";
import List from "./List";
import Details from "./Details";

import Amplify, { API } from "aws-amplify";
import aws_exports from "./aws-exports";
import { withAuthenticator } from "aws-amplify-react";
Amplify.configure(aws_exports);

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      content: "",
      title: "",
      list: [],
      item: {},
      showDetails: false
    };
  }

  async componentDidMount() {
    await this.fetchList();
  }
  handleChange = event => {
    const id = event.target.id;
    this.setState({ [id]: event.target.value });
  };

  handleSubmit = async event => {
    event.preventDefault();
    await API.post("todosApi", "/items", {
      body: {
        id: Date.now().toString(),
        title: this.state.title,
        content: this.state.content
      }
    });

    this.setState({ content: "", title: "" });
    this.fetchList();
  };
  async fetchList() {
    const response = await API.get("todosApi", "/items");
    this.setState({ list: [...response] });
  }

  loadDetailsPage = async id => {
    const response = await API.get("todosApi", "/items/" + id);
    this.setState({ item: { ...response }, showDetails: true });
  };

  loadListPage = () => {
    this.setState({ showDetails: false });
  };

  delete = async id => {
    //TODO: Implement functionality
  };

  render() {
    return (
      <div className="container">
        <form onSubmit={this.handleSubmit}>
          <legend>Add</legend>
          <div className="form-group">
            <label htmlFor="title">Title</label>
            <input
              type="text"
              className="form-control"
              id="title"
              placeholder="Title"
              value={this.state.title}
              onChange={this.handleChange}
            />
          </div>
          <div className="form-group">
            <label htmlFor="content">Content</label>
            <textarea
              className="form-control"
              id="content"
              placeholder="Content"
              value={this.state.content}
              onChange={this.handleChange}
            />
          </div>
          <button type="submit" className="btn btn-primary">
            Submit
          </button>
        </form>
        <hr />
        {this.state.showDetails ? (
          <Details
            item={this.state.item}
            loadListPage={this.loadListPage}
            delete={this.delete}
          />
        ) : (
          <List list={this.state.list} loadDetailsPage={this.loadDetailsPage} />
        )}
      </div>
    );
  }
}

export default withAuthenticator(App, true);
Enter fullscreen mode Exit fullscreen mode

We imported the Amplify library and initialised it by calling Amplify.configure(aws_exports);. When the component is mounted, we call fetchList() to retrieve items from the API. This function uses the API client from the Amplify library to call the REST API. Under the hood, it utilizes Axios to execute the HTTP requests. It'll add necessary headers to the request so you can successfully call the REST API. You can add headers if you defined custom headers for your API, but for our project, we only specify the apiName and path when invoking the functions from the API client. The loadDetailsPage() function fetches a particular item from the database through the API and then sets item state with the response and showDetails to true. This showDetails is used in the render function to toggle between showing a list of items or the details page of a selected item. The function handleSubmit() is called when the form is submitted. It sends the form data to the API to create a document in the database, with columns id, title and content, then calls fetchList() to update the list. I left the delete() function empty so you can implement it yourself. What better way to learn than to try it yourself 😉. This function will be called from the delete button in the Details component. The code you have in it should call the API to delete an item by id and display the list component with correct items. We wrapped the App component with the withAuthenticator higher order component from the Amplify React library. This provides the app with complete flows for user registration, sign-in, signup, and sign out. Only signed in users can access the app since we're using this higher order component. The withAuthenticator component automatically detects the authentication state and updates the UI. If the user is signed in, the underlying App component is displayed, otherwise, sign-in/signup controls are displayed. The second argument which was set to true tells it to display a sign-out button at the top of the page. Using the withAuthenticator component is the simplest way to add authentication flows into your app but you can also have a custom UI and use set of APIs from the Amplify library to implement sign-in and sign up flows. See the docs for more details.

We have all the code necessary to use the application. Open the terminal and run npm start to start the application. You'll need to signup and sign in to use the application.

npm-start.gif

Wrapping Up

We went through creating our backend services using the Amplify CLI. The command amplify add api took us adding resources for DynamoDB, Lambda, API Gateway, and Cognito for authentication. We updated the code in backend/function/todosLambda/src/app.js to match our API requirement. We added UI components to perform CRUD operations on the app and used a higher order component from the Amplify React library to allow only authenticated users access to the application. You should notice we only used a few lines of code to add authentication flows and call the API. Also creating the serverless backend services and connecting them all together was done with a command and responding to the prompts that followed. Thus showing how AWS Amplify makes development easier.

Originally posted on my blog.

Top comments (0)