Build an AWS Serverless Web App with Amplify, Cognito, API-Gateway, Lambda and DynamoDB.

Ghazanfar Ali
7 min readJul 19, 2024

--

Introduction:
In this tutorial, you will create a serverless web application that enables users to request unicorn rides from the Wild Rydes fleet. The application will present users with an HTML-based user interface for indicating a pick-up location and a RESTful web service on the backend to submit the request for dispatching a unicorn. The application will also provide facilities for users to register with the service and log in before requesting rides.

Prerequisites

To complete this tutorial, you will need an AWS account, aws cli installed.
Application Architecture:

Amplify Console provides continuous deployment and hosting of the static web resources including HTML, CSS, JavaScript, and image files which are loaded in the user’s browser. JavaScript executed in the browser sends and receives data from a public backend API built using Lambda and API Gateway. Amazon Cognito provides user management and authentication functions to secure the backend API. Finally, DynamoDB provides a persistence layer where data can be stored by the API’s Lambda function.

Lets start the project!

First we will create a code commit repo for our project:

After creating the repo create the IAM user with git credentials:

Also generate the https code commit creds for this user it will help to clone the repo:

Now we will clone our repo:

On terminal:

Change directory into your repository and copy the static files from S3 using the following commands (make sure you change the Region in the following command to copy the files from the S3 bucket to the Region you selected at the beginning of this tutorial):
⇒cd wildrydes-site
⇒ aws s3 cp s3://wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website ./ — recursive
Add, commit, and push the git files.

The following code block is an example of what you will see in your terminal window:

$ git add .

$ git commit -m “new files”

$ git push

Counting objects: 95, done.

Compressing objects: 100% (94/94), done.

Writing objects: 100% (95/95), 9.44 MiB | 14.87 MiB/s, done.

Total 95 (delta 2), reused 0 (delta 0)

To https://git-codecommit.us-east-1.amazonaws.com/v1/repos/wildrydes-site

* [new branch] master -> master

Now we will built the front end on AWS amplify:
Select the code commit → repo→ branch

Once app create new IAM role with code commit full permission and assign this role to the amplify app:

After deployment amplify will provide a link to access the website:

Amplify also manages the pipeline itself whenever you push any changes in the repo branch it will deploy the changes on environment itself.
Now we will move to second part which is creating the AWS cognito user pool for users authentication:

In MFA section pick no MFA, enable self registration, choose send email with cognito, put the userpool and app client name, leaves all setting default and create te user pool.

Now update the website config in config .js file:

Save the file and push on the repo.

Now go to your website and click on “Giddy up” and register the user here it will available in the users section of your userpool:

Now we will setup our serverless backend with lambda , dynamo and api gateway.
Create the dynamodb table first:

And put all the settings as default, create table. Copy the ARN of table we will require it in lambda function.
Before creating the lambda we will create the IAM role of it:

Also add dynamodb permission to this role:

Put the dynamodb table ARN in resource:

Lets create the lambda function:

Use the below code for lambda:
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’: ‘*’,

},

});

}

Now we will move to API gateway that will receive the traffic from front end and route to the backend using APIs.

Create new resource in the API:

Now create the method for this resource:

Now we need to add user pool for authorization of this route, first create the authorizor:

Now add it to the resource:

Add authorizer for this method:

At last deploy the API, and copy the URL and add it to the config file, so front end will generate request for backend using this url:

Also update the ride.html file:
Replace the argos version 4.3 to 4.6:

Now test the application:

Thats all in the project.

--

--

Ghazanfar Ali

I write technical blogs on DevOps and AWS, Azure and GCP