Build a Three-Tier Web App

Build a scalable web app with S3, CloudFront, Lambda, API Gateway, and DynamoDB.

Introduction

โšก๏ธ 30 second Summary

Welcome to this project on three-tier architecture! ๐ŸŽ‰

Three-tier architecture is a way to organize web applications. It divides an app into three tiers to make your application easier to manage and scale.

Think of it like a layer cake: the top layer (the presentation tier) is what the user sees and interacts with, the middle layer (the logic tier) is the brains of your app that processes data, and the bottom layer (the data tier) stores data in a database.

This is not required, but we recommend learning about each tier separately before starting this project:

  1. Presentation Tier: Website Delivery with CloudFront
  2. Logic Tier: APIs with Lambda + API Gateway
  3. Data Tier: Fetch Data with AWS Lambda

Note: If you're starting this project without doing the recommended projects, we'd suggest giving yourself at least four hours to complete it!

In this project, get ready to...
  • ๐Ÿชฃ Create a storage bucket for your website's files with S3.
  • ๐ŸŒŽ Distribute your content globally with CloudFront.
  • โš™๏ธ Build the brains of your application using serverless functions with Lambda.
  • ๐Ÿšช Create an API to handle user requests with API Gateway.
  • ๐Ÿ’พ Store and retrieve user data with DynamoDB.
  • ๐Ÿ”— Connect all these services together seamlessly for your three-tier architecture.

Want a complete demo of how to do this project, from start to finish? Check out our ๐ŸŽฌ project walkthrough ๐ŸŽฌ

If you're up for a bit of a challenge, quiz yourself on the key concepts up ahead in this project.

This project is part of a series:

  1. Part 1: Website Delivery with CloudFront
  2. Part 2: APIs with Lambda + API Gateway
  3. Part 3: Fetch Data with AWS Lambda

Before we start Step #1...

It's always good to understand exactly what you're here to do.

Set Up the Presentation Tier

We'll start with the top layer, the presentation tier, which is responsible for displaying the website to our users.

In this step, get ready to:

  • Create an S3 bucket to store your website's files.
  • Upload a simple index.html file to your bucket.
  • Set up CloudFront to deliver your website's content globally.

Create an S3 Bucket

First, we need a place to store our website's files. This is where S3 comes in! S3 (Simple Storage Service) is like a massive, scalable hard drive in the cloud.

  • Log in to the AWS Management Console as your IAM Admin user.
  • Make sure you're in the AWS region closest to you.
  • Head to the S3 console.
  • Click Create bucket.
    • Enter here so that you can copy it throughout the project:
nextwork-three-tier-[[UNIQUENAME="enter your name-random characters"]]
  • Replace with your name and a random string of characters.

Why a unique bucket name?

S3 bucket names must be globally unique because they form part of the website address if you enable static website hosting. If two buckets had the same name, there would be a conflict when trying to access the websites they host.

  • Leave all other settings as default.
  • Select Create bucket.
  • Click into your created bucket.

Upload Website Files

Now that we have our storage bucket set up, let's fill it with the actual content of our website.

What is index.html?

index.html is the main file for a website. It's where you organise the text, pictures, and everything that makes up your webpage.

๐Ÿ’ก What is style.css?

style.css is where you write down the visual appearance of your website's HTML elements. It controls everything from font sizes and colors to layout designs, helping you keep a consistent style across your website.

๐Ÿ’ก What is script.js?

script.js is a JavaScript file that adds interaction to your website. It's where you would write the instructions for making things on your website move or change when you click a button or submit a form.

  • Head to the Downloads folder in your local computer. Let's see if you can find the files you've downloaded.
  • Open index.html in your browser.

How can I open index.html in my browser?

  1. Right click on index.html.
  2. Select Open With > select your preferred browser.
  • Do you see a simple web page?
  • Nice work. If you see a web page, you're on your way to setting up your website's presentation!

Am I setting up a static website?

Not quite! Whether a site is static website or a web app depends on where the code is run:

  • Static websites don't need to communicate with any backend cloud resources, like databases, so code or logic is run on the client side i.e. the browser.
  • Web apps, on the other hand, execute code on the server side. For example, apps that use databases, APIs or Lambda functions in the backend. We'd be setting up a static website if we hosted this website right away without setting up our logic or data tiers. Since we're building a three-tier architecture today, there's going to be come code running on the server side (i.e. our Lambda function)!

Upload Your Website Files

  • Head back to the S3 console.
  • Select Upload.
  • Select Add files.
  • Select the three website files in your Downloads folder.
  • Select Upload.
  • Upload success!

Create a CloudFront Distribution

Now that our website files are uploaded, it's time to introduce Amazon CloudFront, our secret weapon for delivering it all over the world.

  • Head to the CloudFront console.

What is Amazon CloudFront?

Amazon CloudFront is a Content Delivery Network (CDN), which means it speeds up the distribution of your static and dynamic web content, such as .html, .css, .js, and image files.

๐Ÿ’ก How does CloudFront speed up distributions?

By using caching!

Caching is the process of storing copies of files in a cache, i.e. a temporary storage location, so that they can be accessed more quickly.

CloudFront has servers in many locations around the world. When a user requests content from your website, CloudFront checks if it has a copy of the content in a server near the user. If it does, it delivers the content from that server.

This is much faster than delivering the content from your origin server, which might be located in a different part of the world.

  • Select Create a CloudFront distribution.

What is a CloudFront distribution?

A CloudFront distribution is a set of instructions that tells CloudFront how to deliver your content.

It specifies where your website's files are stored (called the origin), how they should be cached, and other delivery settings like security standards.

  • In the Distribution options panel, enter a name to match your S3 bucket in the Distribution name.
  • Now for the Distribution type, select the Single website or app option.
  • Select Next.
  • In the Origin panel, select the Browse S3 button.
  • Select your bucket name and click Choose.
  • Keep the default in the Settings panel and select Next at the bottom.

What are CloudFront Settings?

CloudFront settings help you control how your files are delivered from your storage (like S3) to users around the world. These settings include how CloudFront connects to your storage, who can access your files, and how quickly updates show up for users.

Most beginners use the recommended settings, which balance speed, security, and simplicity.

๐Ÿ’กWhat does โ€œAllow private S3 bucket access to CloudFrontโ€ mean?

If your S3 bucket is private, CloudFront can be given special permission to fetch files for you. This keeps your files locked up from the public but lets CloudFront deliver them quickly to your users.

Itโ€™s like giving CloudFront a private key to your storage room, so only it can get in and share your stuff.

  • For Web Application Firewall (WAF), select Do not enable security protections.

What is Web Application Firewall (WAF)?

AWS WAF is a web application firewall. A WAF protects your website from common threats. For example, a WAF could block traffic from IP addresses that are known for malicious behaviour.

  • Let's review the configuration. Select Create distribution if everything looks good!
  • Select Next.

Phew! You've just set up a CloudFront distribution from scratch - nice work.

Update your S3 bucket's settings

in your CloudFront distribution's settings page, select Copy policy.

  • Next, select the shortcut under the popup message. It lets you go straight to your S3 bucket's Permissions tab.
  • Made it!
  • In your S3 bucket's Permissions page, scroll to the Bucket policy section.
  • Select Edit.
  • Paste the policy that you copied into the policy editor.
  • Select Save changes.

Verify Your CloudFront Distribution

Now, let's check if our website is live!

  • Head back into your CloudFront console.
  • Copy the distribution domain name. This is the URL that CloudFront will use to serve your website.
  • Paste the domain name into your web browser.

Yay! Congrats on distributing your website over CloudFront. This ticks off the presentation tier, which is all about the interface that your users and see and interact with.

Set Up the Logic Tier

Now that we have our presentation tier set up, let's move on to the logic tier.

This is where the magic happens! The logic tier is responsible for handling the brains of the application, such as fetching data from a database and performing calculations.

In this project, our logic will be a simple Lambda function that retrieves user data from a DynamoDB table. We need a way to expose that functionality to the outside world, so we'll use API Gateway to handle requests and route them to the right place.

In this step, get ready to:

  • Create a Lambda function to fetch data from a DynamoDB table.
  • Write the code for your Lambda function.
  • Create an API Gateway REST API.
  • Create a resource and method to handle GET requests.
  • Deploy the API to make it accessible.

Create a Lambda Function

Let's create a Lambda function. This function will fetch data from a database and return it to the user. This is a very common use case in web apps.

  • Head to the Lambda console.

What is AWS Lambda?

AWS Lambda is a service that lets you run code without needing to manage any computers/servers - Lambda will manage them for you.

Lambda runs your code only when you need it to (so you're not paying for any idle time).

It also scales automatically, from a few requests per day to thousands per second. All you need to do is supply your code in one of the languages that Lambda supports.

  • Click Create function.
  • Select Author from scratch.
  • For Function name, enter RetrieveUserData.
  • For Runtime, select a runtime using Node.js.

What does runtime mean?

The runtime is like the environment where your code lives and runs.

While you write code in a programming language, the runtime provides the actual environment where that code runs. For example, Python code needs a Python runtime environment.

  • For Architecture, select x86_64.
  • Select Create function.
  • Success!

How do Lambda functions work?

Lambda functions are triggered by events, such as an HTTP request or a change in an S3 bucket. When triggered, the Lambda service executes your code in the runtime environment you've picked.

You don't have to worry about managing servers or scaling your application; Lambda takes care of all of that for you (that's why it's called a serverless service).

Write Lambda Function Code

  • Scroll down to the Code source panel.
  • Copy and paste the following code into the code editor, replacing YOUR_REGION with your actual AWS region (e.g., 'us-west-2'):
// Import individual components from the DynamoDB client package
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";

const ddbClient = new DynamoDBClient({ region: '[[YOURREGION="YOUR_REGION"]]' });
const ddb = DynamoDBDocumentClient.from(ddbClient);

async function handler(event) {
    const userId = event.queryStringParameters.userId;
    const params = {
        TableName: 'UserData',
        Key: { userId }
    };

    try {
        const command = new GetCommand(params);
        const { Item } = await ddb.send(command);
        if (Item) {
            return {
                statusCode: 200,
                body: JSON.stringify(Item),
                headers: {'Content-Type': 'application/json'}
            };
        } else {
            return {
                statusCode: 404,
                body: JSON.stringify({ message: "No user data found" }),
                headers: {'Content-Type': 'application/json'}
            };
        }
    } catch (err) {
        console.error("Unable to retrieve data:", err);
        return {
            statusCode: 500,
            body: JSON.stringify({ message: "Failed to retrieve user data" }),
            headers: {'Content-Type': 'application/json'}
        };
    }
}

export { handler };

What does this code do?

This code sets up a Lambda function that retrieves data from a DynamoDB table.

It looks for specific user data based on a userId and returns that data. If there's an error e.g. the userId doesn't exist in the database, it returns an error message.

๐Ÿ’ก How do Lambda functions interact with other AWS services?

Lambda functions can interact with other AWS services using the AWS SDK. The SDK gives you a set of libraries and tools for accessing various AWS services, such as DynamoDB, S3, and API Gateway.

In our case, we're using the DynamoDB functions within SDK to fetch data from our database.

  • Check: Make sure you've updated the placeholder region YOUR_REGION to your own region code.

Deploy the function

  • Select Deploy. This saves your code and makes the function ready to use.
  • Check for the Deployment successful in the bottom right corner of the console.

Set up API Gateway

Now that we have our Lambda function ready, we need a way to access it. This is where API Gateway comes in.

What is an API?

An API, or Application Programming Interface, is a way for different software systems to talk to each other. It's like a messenger that carries requests and responses between systems.

In this project, we're creating an API that carries requests from your user's browser to your Lambda function.

  • In the AWS Management Console, head to the API Gateway console.

What is Amazon API Gateway?

Amazon API Gateway is an AWS service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale.

It manages incoming traffic, directing them to the correct services, and makes sure only authorized requests get through.

๐Ÿ’ก What is the relationship between APIs and Lambda?

API Gateway acts as the "front door" to our Lambda function. It receives requests and then forwards them to Lambda functions for processing.

Lambda processes the request, then sends the response through the API Gateway back to the user.

Create a new API

  • Scroll down the list of available API types.

What are the different API types?

API Gateway supports different types of APIs, like REST, HTTP, and WebSocket. Each type is suited for different use cases, whether itโ€™s maintaining a standard web API (REST), providing real-time capabilities (WebSocket), or routing requests (HTTP).

  • Find REST API.

What is a REST API?

A REST API (Representational State Transfer) is type of API that uses HTTP methods to interact with resources (more on HTTP methods later in this step).

REST is popular because it's simple and can be used with virtually any programming language. We're using a REST API today to set up an API that connects the user with your Lambda function.

  • Select Build.

Configure the API

  • Under API details, select New API.
  • For API name, enter UserRequestAPI.
  • For API endpoint type, select Regional.

What are endpoint types?

Endpoint types define the scope of your API's availability. Regional endpoints are accessible within a specific AWS region, which is great for localized applications because it has low latency for clients in that region.

๐Ÿ’ก What are the other endpoint types?

Besides Regional, there are Edge-Optimized (for global applications) and Private (for internal networks) endpoints.

Each serves different needs depending on how and where you want your API accessed.

  • Select Create API.

What's in my API's page?

Your APIโ€™s page in the AWS Console shows you a dashboard view of your API, including its methods, deployments, and settings. It's the central hub for managing and monitoring your API.

Nice work setting up that API! The API will be the front door to our Lambda function, letting users access the function via HTTP requests.

Next, we'll figure out how our app can be the bridge between our users and the API. In other words, how can users send requests? That's where API resources come in!

Set up an API Resource

  • Under Resources, select Create resource.

What are API resources?

API resources are endpoints that handle different parts of your API's functionalities.

For example, an API for a messaging app might have separate resources for retrieving messages and for retrieving user profiles.

  • For Resource name, enter users.

What are resource path and resource name?

A resource path is the URL path that gives you access to a resource, e.g. /messages and /users would take you to different resources, so you would access different functionalities in that API.

The resource name is used in the API Gateway to refer to that resource, helping you manage and reference it easily in the console.

  • Select Create resource.
  • Select the /users resource.

Woohooo! We're getting really close now. Great work creating the API resource.

To round off our API's setup, let's create an API method. Methods are things you can in a resource.

Set up an API Method

  • In the Methods panel, select Create method.
  • Select GET from the Method type dropdown.

What are methods?

API methods are actions you can do in a resource.

They are based on standard HTTP methods, which are different commands that let you interact with data over the internet. For example:

  1. GET to retrieve,
  2. POST to add,
  3. PUT to update, and
  4. DELETE to remove data.

Configure the method

  • Select Lambda Function for the Integration type.

What are integration types?

Integration types are the backend services that can fulfill an API request. This setting determines how API Gateway passes the request data to the backend and processes the response.

By selecting a Lambda function as the integration type, API Gateway can directly call your AWS Lambda function each time someone uses an API endpoint:

  1. When API Gateway gets a request, it sends it directly to your Lambda function.
  2. Your Lambda function takes care of the request, processes it, and sends the result back to API Gateway.
  3. API Gateway passes that response on to the user.
  • Switch on Lambda proxy integration.

Extra for Experts: What is a Lambda proxy integration?

Lambda proxy integration is a setting that simplifies the connection between API Gateway and Lambda.

Normally, when the user interacts with the website, a request (e.g. a request for data from a database) goes straight to the API Gateway. This request contains multiple parts, like headers, query parameters, path parametres and more.

The API Gateway has the massive task of breaking down the request and reformat it in a way that the Lambda function can process. Once Lambda returns a response (e.g. retrieved data from the database), API Gateway needs to map the response back into a format expected by the client.

With Lambda proxy integration, API Gateway doesn't need to reformat the user's request. Instead, it passes the entire request - headers, query parameters, path parameters, and body directly to Lambda. The Lambda function itself will have to be capable of processing the request internally.

  • For the Lambda function, make sure the default region selected is where you've created your function.
  • Select your RetrieveUserData function.

Why do I select a Lambda function?

The Lambda function you select will be the function that gets triggered when that API method is called.

In this case, when the GET method for the /users resource is called, API Gateway will pass that request to the Lambda function you set up. When the function runs, Lambda will retrieve user data in a DynamoDB table.

  • Select Create method.
  • Method created!

Method done = API setup DONE! ๐Ÿ˜ฎโ€๐Ÿ’จ

Let's recap: What are resources and methods in API Gateway?

Resources represent the different parts of your API. In our case, the /users resource represents the collection of users.

Methods define the actions that can be performed on a resource. The GET method is used to retrieve data.

That's great, we'll just need to deploy our API and see it in action.

Deploy the API

  • Select Deploy API.
  • For Stage, select New stage.

What does stage mean in Lambda?

In API Gateway, a stage is a snapshot of your API at a specific point in time.

API Gateway lets you deploy different versions of your API to different stages. This way, you can easily control who accesses what version of your API and when.

Usually, developers work on new features or changes in a development stage of the API, test new features in the testing stage, then deploy in in the production stage.

  • For Stage name, enter prod.

Why is the stage called prod?

Prod stands for production.

Production is the live environment where your API is fully working, and there is live traffic and real users using the API too.

  • Select Deploy.
  • Deployment success!

What is on the 'prod' stage's page?

This page shows you settings, metrics, and logs for your API when itโ€™s in production.

Visit your API

  • On the same page, find your prod stage's Invoke URL.

What is the invoke URL?

The invoke URL is the URL where your API can be used.

In real world scenarios, developers use the prod stage's invoke URL into their live application's code, so users are using the live/production version of the API.

  • Copy the Invoke URL.
  • Access the URL in a new tab on your browser.

Dang it - you'll get an error because we haven't set up our DynamoDB table yet. That's okay! We're getting to that next ๐Ÿ˜‰

Set Up the Data Tier

We've got website files distributed through CloudFront, and a Lambda function that's ready to retrieve data.

Now, let's put our API to use. The data tier is where you store all the data that your application uses.

We'll use DynamoDB to store some user data.

In this step, get ready to:

  • Create a DynamoDB table.
  • Add user data into your table.

Create a DynamoDB Table

DynamoDB is our NoSQL database. It's fast, flexible, and perfect for storing user data.

  • Head to the DynamoDB console.

What is DynamoDB?

DynamoDB is one of AWS's database services. It stands out as a fast and flexible way to store data, which makes it a great choice for apps that need quick access to large volumes of data e.g. games.

If you enjoy using DynamoDB in this project, make sure to check out the Load Data into DynamoDB project! - Make sure you're still in the same region as your Lambda function and S3 bucket.

  • Select Create table.
  • For Table name, enter UserData.
  • For Partition key, enter userId.

What is a partition key?

A partition key is the heart of how DynamoDB organizes data. Think of it as a label that you can use to group similar items. Under the hood, the partition key is how DynamoDB spreads out your data across different servers for quick access and efficient querying.

Every item in your table must have a unique partition key.

  • Select String as the data type for the partition key.
  • Leave the default settings for the rest of the options.
  • Select Create table.

Add a table item

Now that we have our DynamoDB table set up, let's add some sample data so we can see our Lambda function in action later.

  • We'll wait until the table status changes to Active. While we wait...
  • Once the table status changes to Active, select your UserData table.
  • Select Explore table items.
  • At the Items returned panel, select Create item.
  • Select Switch to JSON view.

Why is there a JSON view in DynamoDB?

Under the hood, DynamoDB stores your data in JSON! By switching to JSON view, you can edit your data in a code format instead of filling out a form.

This is also a great way to save time - if you have data with lots of attributes, you wouldn't want to fill each of them out one by one for every item.

  • Switch off View DynamoDB JSON.

What is DynamoDB JSON?

DynamoDB JSON is bit more specific than regular JSON. On top of storing the data's value, it also tells your database each data's type (e.g. a string or a number).

We won't be writing in DynamoDB JSON to stick with the simpler JSON format, so we can turn this off.

  • Paste the following JSON into the editor:
{
  "userId": "1",
  "name": "Test User",
  "email": "test@example.com"
}

What does this JSON code say?

This JSON code defines a new item for our UserData table.

This item represents a user with...

  • A userId of 1
  • A name of Test User, and
  • An email of test@example.com

Notice how we didn't have to add name or email as new attributes beforehand!

DynamoDB is schemaless, meaning you can add attributes as you need, and every item in your database can have a different set of attributes. This flexibility is one of the key benefits of using a NoSQL database like DynamoDB.

  • Select Create item.
  • Verify that the item was created successfully. You should see it listed in the Items returned tab.

Phew! That's a piece of data in our DyanmoDB table now.

With the data tier ticked off, we're officially ready to merge the three layers!

Well, almost.

Don't forget to open up the gates between Lambda and DynamoDB - we still need to give Lambda the permission to read the items in your database table.

Grant DynamoDB access to Lambda

  • Head back to your Lambda console.
  • Switch to the Configuration tab in your Lambda function.
  • Select Permissions.

What is in the Permissions tab?

The Permissions tab shows the execution role linked with your Lambda function and the permissions given to that role. It's where you control what your function is allowed to access.

  • Select the execution role name (it will look something like RetrieveUserData-role-xxxxxxxx).
  • This shortcut will take you to the IAM console, with your Lambda function readily open.
  • Select Add permissions.
  • Select Attach policies.
  • Type DynamoDB in the search bar.
  • Select AmazonDynamoDBReadOnlyAccess as the permission policy we'll use.

Extra for Experts: What about "AmazonDynamoDBFullAccess"?

AmazonDynamoDBFullAccess lets you do everything with DynamoDB, like creating, deleting, and managing tables. It also lets you read and write data.

But, if your Lambda function only needs to read data, you don't need this level of access. Granting this permission when you don't need it can make your resources less secure. For example, if someone gets unauthorized access to your function, they could manipulate or delete crucial data by editing your function, which could disrupt your service or lead to data loss.

  • Select Add permissions.

Yay! Permissions added. This means your Lambda function should be able to read DynamoDB table items.

With the data tier officially ticked off, now we're officially ready to merge the three layers!

Integrate the Tiers

We've built all three tiers of our application!

Now, it's time to connect them. We'll update our index.html file to make a request to our API Gateway endpoint and display the returned data.

In this step, get ready to:

  • Update your script.js file with JavaScript code to make an API request.
  • Verify that the data is displayed on your website.

Verify API Functionality

Let's test our API:

  • Head back into your API Gateway console.
  • Copy your prod stage API's Invoke URL.
  • Append /users?userId=1 to the end of the URL you've copied.
  • Run the edited URL in your web browser.
  • You can see your table's data getting returned by the API.

That's the logic and data tier's integration verified โœ…

Verify the distributed website

Now let's check our distributed website on CloudFront.

  • Find your distributed site's URL in the CloudFront console again.
  • Open the URL in your browser.
  • Try entering 1 in the userId field and selecting Get User Data.
  • Do you see data returned to you?

OOo not yet...

Let's investigate what's happened.

Update the connection between the presentation and logic tiers

Where do you think your website is connected to your Lambda function?

  • You can troubleshoot frontend errors using your browser's developer tools.
  • Open your browser's developer tools, usually by pressing F12 on the keyboard.

What are my browser's developer tools?

Browser developer tools are built into most modern web browsers (e.g. Safari, Google Chrome) and gives you options to inspect the HTML, CSS, and JavaScript of a webpage.

Developer tools are also used to debug errors and see a website's performance e.g. how fast content gets loaded.

  • Select the Console tab.
  • Refresh your page.
  • Aha! There's an error... it's referencing a URL https://[YOUR-PROD-API-URL]/users?userId=1

Where is this URL?

Read the entire error message - notice that it actually references where you can find this URL:

That's right, this URL is in line 9 of our script.js file.

  • Open your local computer's Downloads folder.
  • Open script.js in a code/text editor.
  • Aha, there's a line that directly references [YOUR-PROD-API-URL]
  • Looks like we've found our missing piece. We still need to update a placeholder that's inside script.js.
  • Head back into your API Gateway console.
  • Select Stages from the left hand sidebar. If you can't find it, make sure you've selected UserRequestAPI from the console homepage.
  • Copy your prod stage API's Invoke URL.
  • Paste this in script.js, making sure you're replacing [YOUR-PROD-API-URL] in the script.
  • Save your changes.

Upload the Updated script.js

  • Head back to the S3 console.
  • Select Upload.
  • Select Add files.
  • Select the updated script.js in your Downloads folder.
  • Select Upload.
  • Upload success!

Niceeeeee, that's your script.js all updated. High five! ๐Ÿ™Œ

Validate a Fully Functioning Web App

It's time to validate our web app one more time - can we search for items in the application now?

In this step, get ready to:

  • Test your CloudFront site again.
  • Resolve an error on the browser side (yup, one last error coming through)!

Verify Your CloudFront Site

  • Access your website through the CloudFront URL again.
  • Can you see your data now?

Dang it, it's still a no!

  • Use the Console developer tool to investigte your error this time.

What is the error this time?

The CORS (Cross-Origin Resource Sharing) error you're encountering happens because your API Gateway is not configured to allow requests from your CloudFront URL.

API Gateway is only allowing requests directly from its Invoke URL!

To resolve this, you'll need to enable CORS on your API Gateway so that it can accept requests from the domain where your frontend is hosted.

๐Ÿ’ก What is CORS?

CORS (Cross-Origin Resource Sharing) is like a security bouncer for your browser. It decides whether your frontend (like your CloudFront-hosted site) is allowed to talk to a backend server (like API Gateway).

By default, browsers are super cautious - they donโ€™t let one website access resources from another domain (e.g. your frontend trying to call your API Gateway) unless the backend explicitly says it's okay.

In this case, because CORS isnโ€™t configured in API Gateway, the browser freaks out and blocks the request from CloudFront, giving you those annoying CORS errors.

So, we need to enable CORS on your API Gateway. This is like handing the browser a VIP list that says, "Yep, the CloudFront URL is trusted - let it through."

Configure CORS on API Gateway

  • Head back to the Amazon API Gateway console in your AWS account.
  • Navigate to the Resources tab.
  • Select the /users resource.
  • Select Enable CORS.
  • In the CORS configuration, check both GET and OPTIONS under Access-Control-Allow-Methods.

What is OPTIONS?

OPTIONS is another method that browsers use to do a pre-check of your CORS settings.

  • Enter your CloudFront distribution domain name as the Access-Control-Allow-Origin value. This will allow requests from your CloudFront domain to your API.
  • Select Save.

Deploy Your API

After enabling CORS, you must redeploy your API for the changes to take effect:

  • Select Deploy API.
  • Choose your deployment stage i.e. prod.
  • Click Deploy to update the stage.

Add CORS Headers in Your Lambda Function

What are we doing in our Lambda function?

You're using Lambda Proxy Integration for your GET method (this was st up in Step #2)!

When proxy integration is enabled, API Gateway simply forwards the request to your Lambda function and expects the Lambda to return the full HTTP response, including the CORS headers.

This means the CORS headers must be added directly in your Lambda function response, not within API Gateway.

  • Update your Lambda function code to include the Access-Control-Allow-Origin header in the response:
// Import individual components from the DynamoDB client package
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";

const ddbClient = new DynamoDBClient({ region: '[[YOURREGION="YOUR_REGION"]]' });
const ddb = DynamoDBDocumentClient.from(ddbClient);

async function handler(event) {
    const userId = event.queryStringParameters.userId;
    const params = {
        TableName: 'UserData',
        Key: { userId }
    };

    try {
        const command = new GetCommand(params);
        const { Item } = await ddb.send(command);

        if (Item) {
            return {
                statusCode: 200,
                headers: {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*' // Allow CORS for all origins, replace '*' with specific domain in production
                },
                body: JSON.stringify(Item)
            };
        } else {
            return {
                statusCode: 404,
                headers: {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                body: JSON.stringify({ message: "No user data found" })
            };
        }
    } catch (err) {
        console.error("Unable to retrieve data:", err);
        return {
            statusCode: 500,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify({ message: "Failed to retrieve user data" })
        };
    }
}

export { handler };
  • Check: Have you updated YOUR_REGION to your region's code?
  • For security best practice, replace * with your CloudFront domain name. Keeping Access-Control-Allow-Origin means you're allowing everyone to use your API, but we should restrict access to just your CloudFront distribution.
  • Select Deploy to deploy your updated function.

The Final Test...

  • Let's do one more refresh of our CloudFront domain name.
  • WOAHH, you should now see the data fetched from DynamoDB displayed on your website!

What a win!

Delete your resources

Delete your resources

Now that you've successfully built and tested your three-tier web application, it's time to clean up your AWS resources.

This will help us avoid incurring unnecessary costs.

Resources to delete:

  • ๐ŸŒŽ The CloudFront distribution (do this first!)
  • ๐Ÿชฃ The S3 bucket.
  • โš™๏ธ The Lambda function.
  • ๐Ÿšช The API.
  • ๐Ÿ’พ The DynamoDB table.

Note

Make sure to delete the distribution before you delete the S3 bucket.

If you delete the S3 bucket first, your CloudFront distribution will point to a non-existent origin. This could cause some errors with deleting the distribution itself!

  • Delete the CloudFront Distribution
  • In the CloudFront console, select your distribution.
  • Select Disable.
  • Wait for the distribution status to change to Disabled.
  • Select Delete.

The Delete button isn't working.

If the Delete option isn't available, it means CloudFront is still propagating your distribution to edge locations.

Wait a few minutes, until a new timestamp appears under the Last modified column, then try deleting again.

  • Delete the S3 Bucket
  • In the S3 console, select your bucket.
  • Select Empty. Confirm the deletion.
  • Select Delete. Confirm the deletion again.
  • Delete the API
  • Navigate to the API Gateway service in the AWS Management Console.
  • Select your API (UserRequestAPI).
  • Select Actions and select Delete API.
  • Confirm the deletion.
  • Delete the Lambda function
  • Navigate to the Lambda service in the AWS Management Console.
  • Select your function (RetrieveUserData).
  • Select Actions and select Delete.
  • Confirm the deletion.
  • Delete the DynamoDB table
  • Head to the DynamoDB service.
  • From the left hand navigation bar, select Tables.
  • Select the UserData table.
  • Select Delete.

What are these warnings about?

  • Delete all CloudWatch alarms: Keep this checked โœ… Deleting any CloudWatch alarms associated with this table will save you from unnecessary charges.
  • Create an on-demand backup: Keep this unchecked โŒ You can create a backup of your table to save for the long term, so you restore your data to its exact state before deleting it. Additional charges apply for on-demand backup and restore, so we won't use this option.
  • Type confirm to confirm the deletion.
  • Select Delete.

That's a wrap!

That's a wrap!

You just built a three-tier web application on AWS. That's a huge accomplishment!

Give yourself a pat on the back (or a high five to your computer screen).

You've just learned how to:

  • โ˜๏ธ Store website files with S3.
  • ๐ŸŒŽ Deliver content globally with CloudFront.
  • ๐Ÿง  Write serverless code with Lambda.
  • ๐Ÿšช Create and manage APIs with API Gateway.
  • ๐Ÿ’พ Store and retrieve data with DynamoDB.
  • ๐Ÿ”— Connect all these services together to build a fully functional three-tier web application.

Ready to quiz yourself? You got this! ๐Ÿ’ช