DEV Community

Cover image for How To Build a Serverless Web Application - Part B
Sidra Saleem for SUDO Consultants

Posted on

How To Build a Serverless Web Application - Part B

Module 3 – Build a Serverless Backend

Overview

Now comes the building of a backend process that will be used for handling requests of your web application. For this we will use AWS Lambda and Amazon DynamoDB. Previously, we mentioned that users will be allowed to make requests about the delivery of unicorn to a location of their choice. To deal with such requests, a service running in the cloud will be invoked by JavaScript running in the browser.

Architecture Overview

You will need to implement a Lambda function that will be handling user requests regarding unicorn. The flow will be like this: Lambda function will select a unicorn from the fleet, it will use DynamoDB table to record the request and then it will send the details regarding the unicorn being dispatched to the frontend.

Amazon API Gateway is responsible to invoke the function from the browser. However, this connection will be implemented in the upcoming module. This module is meant for testing your function in isolation only.

Implementation

Follow the implementation steps carefully to avoid any inconvenience.

Create an Amazon DynamoDB Table

Make use of DynamoDB console to create a new table.

  1. Visit Amazon DynamoDB console and select “Create Table” option.
  2. Enter “Rides” as the table name (this field is case-sensitive).
  3. For the partition key field, enter “RideId” and select the key type to be String (this field is case-sensitive).
  4. Let the default settings be selected in the “Table Settings” section and select “Create Table” option.
  5. Wait for the table creation process to complete. When the status says “Active”, select your table name.
  6. Visit Overview Tab to navigate to the “General Information” section of your newly created table. Choose “Additional Info” and copy the ARN.

Create an IAM Role for your Lambda Function

An IAM role is associated with every Lambda function. This role is responsible for defining the AWS services the function is allowed to communicate with. For this tutorial, you will have to create an IAM role that gives permission to your Lambda function for writing logs to Amazon CloudWatch Logs and an access for writing items in the DynamoDB table.

  • Visit IAM console and select “Roles” option. Then choose “Create Role”.
  • Select “AWS Service” in the Trusted Entity Type section. Then choose “Lambda” and select “Next”.
  • Go to the filter box and enter “AWSLambdaBasicExecutionRole” and select “Enter”.
  • Select the checkbox present next to “AWSLambdaBasicExecutionRole” and select “Next.
  • Enter the role name to be “WildRydesLambda” while leaving other settings to default.
  • Select “Create Role” option.
  • On the roles page, type “WildRydesLambda” and select the newly created name of the role.
  • Under “Add Permissions” section in the Permissions tab, choose “Create Inline Policy” option.
  • To select a service, type “DynamoDB” in the search bar and select it.
  • Click on “Select Actions”.
  • Look for “Actions Allowed” section and type “PutItem” in the search box. Select its checkbox after it has appeared.
  • Now in the “Resources Section”, choose “Add ARN link”.
  • Select “Text” tab and paste the ARN of your table. Choose “Add ARNs” option.
  • Select “Next”.
  • For the policy name enter “DynamoDBWriteAccess” and choose “Create Policy” option.

Create a Lambda Function for Handling Requests

In this particular step, you will be allowed to create a function that will process API requests regarding unicorn dispatch from the web application. For creating a Lambda function (RequestUnicorn) use AWS Lambda console. This function will process the API requests. Use the below implementation for your function code. Also configure the function in such a way that it uses IAM role you created previously.

  • Visit AWS Lambda console and select “Create a Function” option.
  • Keep the default author to be selected.
  • For function name enter “RequestUnicorn”.
  • Select Node.js 16.x version as any newer versions will not work in this tutorial.
  • From the “Change default execution role dropdown” select “Use an existing role” option.
  • From the dropdown menu of existing roles select “WildRydesLambda”.
  • Select “Create Function” option.
  • Now scroll down to the code source section. All the contents present in index.js file should be replaced with the contents written in requestUnicorn.js.

The following code is present in requestUnicorn.js file. Copy and paste this code in the index.js file.

const randomBytes = require('crypto').randomBytes;

const AWS = require('aws-sdk');

const ddb = new AWS.DynamoDB.DocumentClient();

const fleet = [

    {

        Name: 'Angel',

        Color: 'White',

        Gender: 'Female',

    },

    {

        Name: 'Gil',

        Color: 'White',

        Gender: 'Male',

    },

    {

        Name: 'Rocinante',

        Color: 'Yellow',

        Gender: 'Female',

    },

];

exports.handler = (event, context, callback) => {

    if (!event.requestContext.authorizer) {

      errorResponse('Authorization not configured', context.awsRequestId, callback);

      return;

    }

    const rideId = toUrlString(randomBytes(16));

    console.log('Received event (', rideId, '): ', event);

    // Because we're using a Cognito User Pools authorizer, all of the claims

    // included in the authentication token are provided in the request context.

    // This includes the username as well as other attributes.

    const username = event.requestContext.authorizer.claims['cognito:username'];

    // The body field of the event in a proxy integration is a raw string.

    // In order to extract meaningful values, we need to first parse this string

    // into an object. A more robust implementation might inspect the Content-Type

    // header first and use a different parsing strategy based on that value.

    const requestBody = JSON.parse(event.body);

    const pickupLocation = requestBody.PickupLocation;

    const unicorn = findUnicorn(pickupLocation);

    recordRide(rideId, username, unicorn).then(() => {

        // You can use the callback function to provide a return value from your Node.js

        // Lambda functions. The first parameter is used for failed invocations. The

        // second parameter specifies the result data of the invocation.

        // Because this Lambda function is called by an API Gateway proxy integration

        // the result object must use the following structure.

        callback(null, {

            statusCode: 201,

            body: JSON.stringify({

                RideId: rideId,

                Unicorn: unicorn,

                Eta: '30 seconds',

                Rider: username,

            }),

            headers: {

                'Access-Control-Allow-Origin': '*',

            },

        });

    }).catch((err) => {

        console.error(err);

        // If there is an error during processing, catch it and return

        // from the Lambda function successfully. Specify a 500 HTTP status

        // code and provide an error message in the body. This will provide a

        // more meaningful error response to the end client.

        errorResponse(err.message, context.awsRequestId, callback)

    });

};

// This is where you would implement logic to find the optimal unicorn for

// this ride (possibly invoking another Lambda function as a microservice.)

// For simplicity, we'll just pick a unicorn at random.

function findUnicorn(pickupLocation) {

    console.log('Finding unicorn for ', pickupLocation.Latitude, ', ', pickupLocation.Longitude);

    return fleet[Math.floor(Math.random() * fleet.length)];

}

function recordRide(rideId, username, unicorn) {

    return ddb.put({

        TableName: 'Rides',

        Item: {

            RideId: rideId,

            User: username,

            Unicorn: unicorn,

            RequestTime: new Date().toISOString(),

        },

    }).promise();

}

function toUrlString(buffer) {

    return buffer.toString('base64')

        .replace(/\+/g, '-')

        .replace(/\//g, '_')

        .replace(/=/g, '');

}

function errorResponse(errorMessage, awsRequestId, callback) {

  callback(null, {

    statusCode: 500,

    body: JSON.stringify({

      Error: errorMessage,

      Reference: awsRequestId,

    }),

    headers: {

      'Access-Control-Allow-Origin': '*',

    },

  });

}
  • At last, choose “Deploy”.

Validate your Implementation

In this step, you will test the Lambda function that you just created. Later we will add REST API with API Gateway to invoke this function from the browser application.

  • Go to “RequestUnicorn” function and choose “Test” present in the Code Source section. From the dropdown, select “Configure Test Event”.
  • Leave the selections of “Create New Event” to default.
  • For event name, enter “TestRequestEvent”.
  • Now copy the following test event and paste it into “Event JSON” section.
{

    "path": "/ride",

    "httpMethod": "POST",

    "headers": {

        "Accept": "*/*",

        "Authorization": "eyJraWQiOiJLTzRVMWZs",

        "content-type": "application/json; charset=UTF-8"

    },

    "queryStringParameters": null,

    "pathParameters": null,

    "requestContext": {

        "authorizer": {

            "claims": {

                "cognito:username": "the_username"

            }

        }

    },

    "body": "{\"PickupLocation\":{\"Latitude\":47.6174755835663,\"Longitude\":-122.28837066650185}}"

}
  • Choose “Save” option.
  • Now again visit “Code Source Section” and choose “Test”. From the dropdown select “TestRequestEvent”.
  • Choose “Test” present on the test tab.
  • Expand the “Details” of the succeeded message.
  • Your function result should look something like this.
{

    "statusCode": 201,

    "body": "{\"RideId\":\"SvLnijIAtg6inAFUBRT+Fg==\",\"Unicorn\":{\"Name\":\"Rocinante\",\"Color\":\"Yellow\",\"Gender\":\"Female\"},\"Eta\":\"30 seconds\"}",

    "headers": {

        "Access-Control-Allow-Origin": "*"

    }

}

Module 4 – Deploy a REST FUL API

Overview

This module will deal with exposing the Lambda function as a REST FUL API. This API will be publicly accessed on the internet and will be secured using Amazon Cognito user pool. By making these required configurations, you will transform a statically hosted website into a dynamic one. When client-side JS is added, it will make AJAX calls to all the exposed APIs.

Architecture Overview

The static website can already interact with the API due to the page configuration made. The “/ride.html” page shows a simple map-based interface that is used for requesting unicorn ride. After performing proper authentication using “/signin.html” page, the users will be able to choose pickup location. After this, they can request a ride by clicking on “Request Unicorn” button.

In this module, you will learn to build cloud components of the API. For this case, the application will use ajax() method to make remote requests.

Implementation

Follow the implementation steps carefully to avoid any inconvenience.

Create a New REST API

  1. Go to Amazon API Gateway console and select APIs.
  2. Under REST API choose “Build”.
  3. Select “REST” in the “Choose the Protocol Section”.
  4. Select “New API” in the “Create new API” section.
  5. Enter “WildRydes” as the API name and from the Endpoint Type dropdown choose “Edge Optimized”.
  6. Select “Create API”.

Create Authorizer

Now it is time to create Amazon Cognito User Pools Authorizer. Amazon API Gateway makes use of JWT. These tokens are returned by the user pool for authenticating the API calls. In this particular section, we will be creating an authorizer for API. Follow the below steps to configure the authorizer.

  1. Select “Authorizers” option present in the left navigation pane of the newly created API.
  2. Select “Create New Authorizer” option.
  3. For Authorizer Name enter “WildRydes”.
  4. Select type as “Cognito”.
  5. In the region dropdown menu of the Cognito user pool, select the region you have been following throughout this tutorial. For Cognito User Pool name enter “WildRydes”.
  6. For the token source enter “Authorization”.
  7. Choose “Create” option.
  8. Select “Test” for verifying authorizer configuration.
  9. Copy the authorization token from ride.html page and paste into the Authorization (header) field. Verify that the HTTP status response code is 200.

Create a New Source and Method

You will validate Lambda proxy integration backed by RequestUnicorn function by creating a new resource in your API and then creating a POST method for it.

  1. Select “Resources” present in the left navigation pane of your API.
  2. Click on “Create Resource” from the Actions dropdown menu.
  3. For Resource Name enter “ride” which will create a resource path “/ride”.
  4. Enable API Gateway CORS by selecting the checkbox.
  5. Again choose “Create Resource” option.
  6. From the Actions dropdown menu, select “Create Method”.
  7. From the new dropdown menu that appears under OPTIONS, select “POST”.
  8. For the integration type select “Lambda Function”.
  9. Allow Lambda Proxy Integration by selecting its checkbox.
  10. Select the same region for Lambda Region as well.
  11. For Lambda Function enter “RequestUnicorn”.
  12. Select “Save”.
  13. Choose “Ok” to give Amazon API Gateway the permission to invoke Lambda function.
  14. Choose “Method Request” card.
  15. You will see a pencil icon present right next to “Authorization”.
  16. From the dropdown list, select WildRydes Cognito user pool authorizer. Check the checkmark icon as well.

Deploy your API

In this step, you will deploy your API.

  1. Select the option of “Deploy API” from the Actions dropdown menu.
  2. Select [New Stage] in the “Deployment Stage” list.
  3. For stage name enter “Prod”.
  4. Select “Deploy”.
  5. Make sure to copy the “Invoke URL”.

Update the Website Config

Now you will add the invoke URL in the /js/config.js file. The config file will still have all the information regarding the updates you made in the previous module for userPoolID, Amazon Cognito, userPoolClientID and region.

  • Open the js folder on your machine and then open config.js file in any text editor.
  • Paste the copied invoke URL in the “invokeUrl” value of the config.js file.
  • Save the modified file.

Below is the complete view of config.js file. Your file will have different values from these.

window._config = {

    cognito: {

        userPoolId: 'us-west-2_uXboG5pAb', // e.g. us-east-2_uXboG5pAb        

        userPoolClientId: '25ddkmj4v6hfsfvruhpfi7n4hv', // e.g. 25ddkmj4v6hfsfvruhpfi7n4hv

        region: 'us-west-2' // e.g. us-east-2

    },

    api: {

        invokeUrl: 'https://rc7nyt4tql.execute-api.us-west-2.amazonaws.com/prod' // e.g. https://rc7nyt4tql.execute-api.us-west-2.amazonaws.com/prod,

    }

};
  • Add, commit and push the updated file to the repository.
$ git add .

$ git commit -m "new_configuration"

$ git push

Validate your Implementation

Note: Clear your browser’s cache before executing the following steps.

  • Update ArcGIS JS version to 4.6 in the ride.html file.
<script src="https://js.arcgis.com/4.6/"></script>

 <link rel="stylesheet" href="https://js.arcgis.com/4.6/esri/css/main.css">

Below is the complete example of ride.html file.

<div id="noApiMessage" class="configMessage" style="display: none;">

        <div class="backdrop"></div>

        <div class="panel panel-default">

            <div class="panel-heading">

                <h3 class="panel-title">Successfully Authenticated!</h3>

            </div>

            <div class="panel-body">

                <p>This page is not functional yet because there is no API invoke URL configured in <a href="/js/config.js">/js/config.js</a>. You'll configure this in Module 3.</p>

                <p>In the meantime, if you'd like to test the Amazon Cognito user pool authorizer for your API, use the auth token below:</p>

                <textarea class="authToken"></textarea>

            </div>

        </div>

    </div>

    <div id="noCognitoMessage" class="configMessage" style="display: none;">

        <div class="backdrop"></div>

        <div class="panel panel-default">

            <div class="panel-heading">

                <h3 class="panel-title">No Cognito User Pool Configured</h3>

            </div>

            <div class="panel-body">

                <p>There is no user pool configured in <a href="/js/config.js">/js/config.js</a>. You'll configure this in Module 2 of the workshop.</p>

            </div>

        </div>

    </div>

    <div id="main">

        <div id="map">

        </div>

    </div>

    <div id="authTokenModal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="authToken">

        <div class="modal-dialog" role="document">

            <div class="modal-content">

                <div class="modal-header">

                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>

                    <h4 class="modal-title" id="myModalLabel">Your Auth Token</h4>

                </div>

                <div class="modal-body">

                    <textarea class="authToken"></textarea>

                </div>

                <div class="modal-footer">

                    <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>

                </div>

            </div>

        </div>

    </div>

    <script src="js/vendor/jquery-3.1.0.js"></script>

    <script src="js/vendor/bootstrap.min.js"></script>

    <script src="js/vendor/aws-cognito-sdk.min.js"></script>

    <script src="js/vendor/amazon-cognito-identity.min.js"></script>

    <script src="https://js.arcgis.com/4.6/"></script>

    <script src="js/config.js"></script>

    <script src="js/cognito-auth.js"></script>

    <script src="js/esri-map.js"></script>

    <script src="js/ride.js"></script>

</body>

</html>
  • Save the modified file and add, commit and push it to the Git repository.
  • Visit /ride.html.
  • If you are taken to the ArcGIS sign in page then enter the credentials to proceed further.
  • You will see a map. Click anywhere on the map so you can set a pickup location.
  • Select “Request Unicorn”. You will be notified with a notification that the unicorn is on its way.

Module 5 – Resource Cleanup

Overview

In this module, you will delete all the previously created resources like AWS Amplify App, AWS Lambda function, Amazon Cognito User Pool, DynamoDB table, IAM role, REST API and CloudWatch logs. Doing this will save you from paying extra charges.

Implementation

Follow the implementation steps carefully to avoid any inconvenience.

Delete your App

First of all, delete your Amplify app.

  1. Go to AWS Amplify console and select “wildrydes-site” app.
  2. From the homepage choose “Actions” and then select “Delete App” option. When prompted to confirm, enter delete and also choose delete.

Delete your Amazon Cognito User Pool

  1. Go to Amazon Cognito console and select “WildRydes” user pool name.
  2. Select “Delete User Pool” option.
  3. Right next to Deactivate deletion protection there will be a checkbox that needs to be selected.
  4. To confirm deletion enter “WildRydes” and choose “Delete”.

Delete your Serverless Backend

AWS Lambda Function

  1. Go to AWS Lambda console and select “RequestUnicorn” function.
  2. From the Actions dropdown menu select “Delete” option.

IAM Role

  1. Navigate to IAM console and select “Roles”.
  2. Type “WildRydesLambda” in the filter box.
  3. Select the checkbox and choose “Delete”.
  4. Enter the name of role i.e. “WildRydesLambda” to confirm deletion. Choose “Delete”.

Amazon DynamoDB Table

  1. Go to Amazon DynamoDB console and select “Tables”.
  2. There will be a checkbox next to “Rides” table, select it.
  3. Choose “Delete” option.
  4. Select the checkbox for “Delete all CloudWatch alarms for Rides”, enter confirm in the input box and select “Delete”.
  5. The table will disappear after a few seconds.

Delete your REST API

  1. Go to Amazon API Gateway console and select WildRydes API.
  2. From the Actions dropdown menu, select “Delete”.
  3. Choose Delete to make a confirmation.

Delete your CloudWatch Log

  1. Visit Amazon CloudWatch console, expand logs and select “Log Groups”.
  2. Select the checkbox present next to /aws/lambda/RequestUnicorn log group.
  3. From the Actions dropdown, select “Delete log groups”.
  4. Choose Delete to confirm.

Conclusion

This is the end of the tutorial in which we learned how to build a serverless web application using AWS services. You need to adopt these practices now because then you will have tons of opportunities to come across. Go ahead and practice this tutorial on your own, debug the errors and become an expert.

Top comments (0)