Modern Web Applications with AWS Serverless Architecture
Complete guide to building modern web applications using AWS Serverless services and best practices.

Enes Karakuş
Backend Developer & System Architect
Modern Web Applications with AWS Serverless Architecture#
Serverless architecture is an approach that accelerates development processes and reduces operational burden by eliminating traditional server management. With AWS's rich serverless services, developing high-performance and scalable web applications has never been easier. In this article, we'll explore the process of developing modern web applications using AWS serverless technologies.
Fundamentals of Serverless Architecture#
Contrary to its name, serverless architecture doesn't completely eliminate servers. Instead, it transfers the responsibility of server management to the cloud provider. As a developer, you don't have to worry about server installation, configuration, scaling, and maintenance.
Benefits of Serverless#
- Automation and Simplicity: Eliminating the need for infrastructure management
- Scalability: Automatic and unlimited scaling
- Cost Efficiency: Pay-as-you-go model
- Faster Time to Market: Shortened development cycles
- High Availability: Leveraging AWS's global infrastructure
AWS Serverless Services#
AWS offers a comprehensive set of services for developing serverless applications:
AWS Lambda#
Lambda is the code execution service that forms the foundation of serverless architecture. You upload your code snippets and it runs them in response to specific triggers.
// Simple AWS Lambda function example
exports.handler = async (event) => {
try {
const body = JSON.parse(event.body);
// Process incoming data
const result = processData(body);
// Successful response
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
message: 'Operation successful',
data: result
})
};
} catch (error) {
// Error response
return {
statusCode: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
message: 'An error occurred during processing',
error: error.message
})
};
}
};
function processData(data) {
// Data processing logic
return {
processedAt: new Date().toISOString(),
result: data.value * 2
};
}
Amazon API Gateway#
API Gateway allows you to expose your Lambda functions to the outside world as HTTP endpoints. You can create RESTful APIs and benefit from features like authorization, throttling, and caching.
Amazon DynamoDB#
DynamoDB is a fully managed NoSQL database ideal for the data storage needs of serverless applications. It offers automatic scaling, high availability, and low latency.
// Example of adding data with DynamoDB
const AWS = require('aws-sdk');
const dynamoDB = new AWS.DynamoDB.DocumentClient();
exports.handler = async (event) => {
const data = JSON.parse(event.body);
const params = {
TableName: 'Users',
Item: {
userId: data.userId,
name: data.name,
email: data.email,
createdAt: new Date().toISOString()
}
};
try {
await dynamoDB.put(params).promise();
return {
statusCode: 201,
body: JSON.stringify({
message: 'User created successfully'
})
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({
message: 'An error occurred while creating the user',
error: error.message
})
};
}
};
Amazon S3#
S3 is a service used for file storage needs. It is ideal for hosting static websites, user uploads, and media files.
Amazon Cognito#
You can use Cognito for user authentication and authorization needs. It supports social media accounts, SAML, and OpenID Connect.
AWS CloudFront#
CloudFront is a content delivery network (CDN) service that allows you to serve your application's content with low latency from global edge locations.
Serverless Web Application Architecture#
A modern serverless web application typically consists of these components:
- Frontend: Static assets hosted in S3 (HTML, CSS, JavaScript)
- API Layer: API Gateway + Lambda functions
- Database: DynamoDB or other managed database services
- Authentication: Amazon Cognito
- CDN: CloudFront
Example Application: Serverless Todo App#
To reinforce the theoretical knowledge, let's examine the basic architecture of a simple Todo application.
API Gateway Configuration#
We'll define the following endpoints in API Gateway:
GET /todos
- Lists all todosPOST /todos
- Adds a new todoGET /todos/{id}
- Retrieves a specific todoPUT /todos/{id}
- Updates a todoDELETE /todos/{id}
- Deletes a todo
Lambda Functions#
Let's create a Lambda function for each endpoint:
// getTodos Lambda function
const AWS = require('aws-sdk');
const dynamoDB = new AWS.DynamoDB.DocumentClient();
exports.handler = async (event) => {
const userId = event.requestContext.authorizer.claims.sub; // Cognito user ID
const params = {
TableName: 'Todos',
KeyConditionExpression: 'userId = :userId',
ExpressionAttributeValues: {
':userId': userId
}
};
try {
const result = await dynamoDB.query(params).promise();
return {
statusCode: 200,
headers: corsHeaders,
body: JSON.stringify(result.Items)
};
} catch (error) {
return {
statusCode: 500,
headers: corsHeaders,
body: JSON.stringify({
message: 'An error occurred while retrieving todos',
error: error.message
})
};
}
};
// createTodo Lambda function
exports.handler = async (event) => {
const userId = event.requestContext.authorizer.claims.sub;
const data = JSON.parse(event.body);
const todo = {
todoId: uuidv4(),
userId: userId,
title: data.title,
description: data.description || '',
completed: false,
createdAt: new Date().toISOString()
};
const params = {
TableName: 'Todos',
Item: todo
};
try {
await dynamoDB.put(params).promise();
return {
statusCode: 201,
headers: corsHeaders,
body: JSON.stringify(todo)
};
} catch (error) {
return {
statusCode: 500,
headers: corsHeaders,
body: JSON.stringify({
message: 'An error occurred while creating the todo',
error: error.message
})
};
}
};
DynamoDB Schema#
A simple DynamoDB schema for the Todo application:
{
"TableName": "Todos",
"KeySchema": [
{
"AttributeName": "userId",
"KeyType": "HASH"
},
{
"AttributeName": "todoId",
"KeyType": "RANGE"
}
],
"AttributeDefinitions": [
{
"AttributeName": "userId",
"AttributeType": "S"
},
{
"AttributeName": "todoId",
"AttributeType": "S"
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
}
}
Frontend Integration#
A React example showing how to make requests to our API from the frontend:
import React, { useState, useEffect } from 'react';
import { Auth } from 'aws-amplify';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchTodos();
}, []);
async function fetchTodos() {
setLoading(true);
try {
// Get JWT token from Cognito
const session = await Auth.currentSession();
const token = session.getIdToken().getJwtToken();
// Make request to API Gateway
const response = await fetch('https://api.example.com/todos', {
headers: {
'Authorization': token
}
});
if (!response.ok) throw new Error('Could not retrieve todos');
const data = await response.json();
setTodos(data);
} catch (error) {
console.error('Error:', error);
} finally {
setLoading(false);
}
}
async function addTodo(e) {
e.preventDefault();
if (!newTodo.trim()) return;
try {
const session = await Auth.currentSession();
const token = session.getIdToken().getJwtToken();
const response = await fetch('https://api.example.com/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
body: JSON.stringify({
title: newTodo
})
});
if (!response.ok) throw new Error('Could not add todo');
const addedTodo = await response.json();
setTodos([...todos, addedTodo]);
setNewTodo('');
} catch (error) {
console.error('Error:', error);
}
}
// Render code...
}
Infrastructure as Code with AWS CloudFormation#
You can define all your serverless infrastructure as code with CloudFormation and easily deploy it. Here's a simple example:
AWSTemplateFormatVersion: '2010-09-09'
Resources:
TodosTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Todos
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: userId
KeyType: HASH
- AttributeName: todoId
KeyType: RANGE
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
- AttributeName: todoId
AttributeType: S
GetTodosFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: getTodos
Runtime: nodejs16.x
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile: |
// Lambda code will go here
TodosApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: TodosApi
Description: API for Todo application
# Other resources...
Easier Deployment with Serverless Framework#
Instead of AWS CloudFormation, you can deploy your serverless applications more easily using the Serverless Framework:
service: serverless-todo-app
provider:
name: aws
runtime: nodejs16.x
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'eu-west-1'}
environment:
TODOS_TABLE: ${self:service}-todos-${self:provider.stage}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: !GetAtt TodosTable.Arn
functions:
getTodos:
handler: src/handlers/getTodos.handler
events:
- http:
path: todos
method: get
cors: true
authorizer:
type: COGNITO_USER_POOLS
authorizerId: !Ref ApiGatewayAuthorizer
createTodo:
handler: src/handlers/createTodo.handler
events:
- http:
path: todos
method: post
cors: true
authorizer:
type: COGNITO_USER_POOLS
authorizerId: !Ref ApiGatewayAuthorizer
# Other functions...
resources:
Resources:
TodosTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.TODOS_TABLE}
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: userId
KeyType: HASH
- AttributeName: todoId
KeyType: RANGE
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
- AttributeName: todoId
AttributeType: S
Monitoring Serverless Applications#
You can use AWS CloudWatch to monitor serverless applications. Additionally, you can integrate more advanced monitoring with AWS X-Ray or third-party tools (Datadog, New Relic, etc.).
// Lambda monitoring with X-Ray
const AWSXRay = require('aws-xray-sdk-core');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
exports.handler = async (event) => {
// Create a custom segment for monitoring
const segment = AWSXRay.getSegment();
const subsegment = segment.addNewSubsegment('business-logic');
try {
// Business logic...
subsegment.close();
return {
statusCode: 200,
body: JSON.stringify({ message: 'Success' })
};
} catch (error) {
subsegment.addError(error);
subsegment.close();
return {
statusCode: 500,
body: JSON.stringify({ error: error.message })
};
}
};
Best Practices#
Cold Start Optimization#
To reduce the latency issue (cold start) in the first calls of Lambda functions:
- Prefer runtimes that start quickly, like Node.js or Python
- Keep Lambda functions small and focused
- Use the Provisioned Concurrency feature
- Minimize external dependencies
DynamoDB Data Modeling#
For effective data modeling in DynamoDB:
- Determine your access patterns in advance
- Avoid over-normalization
- Store related data together
- Effectively use GSI and LSI indexes
Security Best Practices#
- Create IAM policies with the principle of least privilege
- Use AWS_IAM or Cognito authorization for API Gateway
- Store sensitive information in AWS Secrets Manager
- Consider running Lambda functions inside a VPC
Cost Optimization#
To optimize the costs of serverless applications:
- Adjust Lambda memory size according to the workload
- Prefer Auto Scaling or On-Demand capacity for DynamoDB
- Use API Gateway caching feature
- Accelerate static content delivery with CloudFront
- Regularly monitor costs with AWS Cost Explorer and Budgets
Conclusion#
AWS serverless architecture offers a powerful, flexible, and cost-effective approach to developing modern web applications. In this article, we've examined the basic components of serverless architecture and how they can be applied on AWS.
The serverless approach enables development teams to focus on product development rather than server management. It offers advantages such as high scalability, low operational cost, and fast time to market.
Serverless approach provides great benefits especially in microservice architectures, new projects, or applications with variable traffic patterns. With the wide range of services AWS offers, you can meet all your modern web application needs.
To maximize the benefits of serverless, it is critical to design your application correctly, think about security from the start, and set up monitoring systems. By laying these foundations correctly, you can take advantage of all the benefits of the serverless world.