Build a Serverless Web App on AWS
Introduction
Serverless architecture eliminates the need for infrastructure management, allowing developers to focus on code rather than servers. In this guide, we will walk through the process of building a serverless web application using AWS services.
What is Serverless Architecture?
Serverless architecture allows applications to run without provisioning or managing servers. AWS provides services like:
- AWS Lambda for executing backend logic.
- Amazon API Gateway for handling HTTP requests.
- Amazon DynamoDB as a NoSQL database.
- Amazon S3 for static website hosting.
- Amazon SNS for event-driven notifications.
Serverless Application Architecture on AWS
1. Hosting a Static Web Application
We use Amazon S3 to host a static website.
- Create an S3 bucket.
- Enable static website hosting in bucket properties.
- Upload the frontend files (HTML, CSS, JavaScript).
- Set bucket permissions for public access.
2. Setting Up API Gateway and Lambda
AWS API Gateway provides an interface for HTTP requests and routes them to AWS Lambda.
- Create an API Gateway with HTTP methods (GET, POST, etc.).
- Configure Lambda functions to process API requests.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110export const handler = async (event) => {
let body;
let statusCode = "200";
const headers = {
"Content-Type": "application/json",
};
try {
console.log("Full event object:", JSON.stringify(event, null, 2));
// Extract HTTP method and validate it
const method = event.requestContext?.http?.method || event.httpMethod;
if (!method) {
throw new Error("HTTP method is missing in the event object.");
}
let tableName;
if (method === "POST" && event.body) {
// For POST, get TableName from the request body
const requestBody = JSON.parse(event.body);
tableName = requestBody.TableName;
if (!tableName) {
throw new Error("TableName is required in the request body.");
}
} else if (method === "GET") {
// For GET, get TableName from the query string
tableName = event.queryStringParameters?.TableName;
if (!tableName) {
throw new Error("TableName is required in the query string.");
}
}
switch (method) {
case "POST": {
// Ensure required fields are present in the body for POST
const requestBody = JSON.parse(event.body);
if (!requestBody.Item) {
throw new Error("Item is required in the request body.");
}
// Step 1: Get the current `nextid` from the record with `id = -1`
const getParams = {
TableName: tableName,
Key: { id: -1 }, // Assuming `id` is a number
};
const { Item: metadata } = await dynamo.get(getParams);
if (!metadata || !metadata.nextid) {
throw new Error("Could not retrieve nextid from the metadata record (id = -1).");
}
const currentNextId = metadata.nextid;
// Step 2: Increment `nextid` for the new item
const newId = currentNextId;
const updatedNextId = currentNextId + 1;
// Step 3: Update the `nextid` in the metadata record
const updateParams = {
TableName: tableName,
Key: { id: -1 },
UpdateExpression: "SET nextid = :nextid",
ExpressionAttributeValues: {
":nextid": updatedNextId,
},
};
await dynamo.update(updateParams);
// Step 4: Insert the new item with the assigned `id`
const newItem = {
...requestBody.Item,
id: newId, // Add the auto-incremented id
};
const putParams = {
TableName: tableName,
Item: newItem,
};
await dynamo.put(putParams);
body = { message: "Item successfully created.", newItem };
break;
}
case "GET":
// Handle GET requests
const scanParams = { TableName: tableName };
const scanResult = await dynamo.scan(scanParams);
body = scanResult;
break;
default:
throw new Error(`Unsupported method "${method}"`);
}
} catch (err) {
console.error("Error occurred:", err.message);
statusCode = "400";
body = { error: err.message };
} finally {
body = JSON.stringify(body);
}
return {
statusCode,
body,
headers,
};
}; - Enable CORS in API Gateway to allow web access.
- Deploy the API and obtain the endpoint URL.
3. Database Integration with DynamoDB
Amazon DynamoDB is used as a NoSQL database for storing customer data.
- Create a DynamoDB table with a partition key
id
. - Use AWS SDK in Lambda to interact with the database.
- Implement API Gateway methods for GET and POST requests.
4. Implementing a Notification System with SNS
AWS SNS allows sending notifications for database changes or file uploads.
- Create an SNS topic and subscribe an admin email.
- Configure Lambda functions to trigger SNS notifications.
- Set up S3 event notifications for file uploads.
Deployment and Testing
1. Testing API Gateway and Lambda
- Use Postman or
curl
to send HTTP requests to the API Gateway endpoint. - Check CloudWatch logs for debugging Lambda execution.
2. Validating DynamoDB Data Storage
- Use AWS Console to inspect DynamoDB records.
- Run queries using the AWS CLI.
3. Ensuring Notification System Works
- Upload a file to the S3 bucket and verify the email notification.
- Add a record to DynamoDB and check SNS messages.
AWS Well-Architected Framework Considerations
Security
- Use IAM roles and policies for access control.
- Encrypt data at rest and in transit.
Reliability
- Enable DynamoDB auto-scaling to handle variable loads.
- Use multi-AZ deployment for resilience.
Performance Efficiency
- Optimize Lambda function execution time to minimize costs.
- Use CloudFront for caching and faster content delivery.
Cost Optimization
- Configure S3 lifecycle policies for storage optimization.
- Minimize Lambda execution time to reduce compute costs.
Video of Serverless Web Applications
Conclusion
Serverless architecture on AWS simplifies application deployment, reduces infrastructure costs, and scales dynamically. By integrating Lambda, API Gateway, DynamoDB, S3, and SNS, you can build a fully functional web application with minimal infrastructure overhead.