#AWS#Serverless#Cloud#Backend

Modern Web Applications with AWS Serverless Architecture

Complete guide to building modern web applications using AWS Serverless services and best practices.

Enes Karakuş

Enes Karakuş

Backend Developer & System Architect

2/20/2025
10 dk okuma

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#

  1. Automation and Simplicity: Eliminating the need for infrastructure management
  2. Scalability: Automatic and unlimited scaling
  3. Cost Efficiency: Pay-as-you-go model
  4. Faster Time to Market: Shortened development cycles
  5. 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:

  1. Frontend: Static assets hosted in S3 (HTML, CSS, JavaScript)
  2. API Layer: API Gateway + Lambda functions
  3. Database: DynamoDB or other managed database services
  4. Authentication: Amazon Cognito
  5. 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 todos
  • POST /todos - Adds a new todo
  • GET /todos/{id} - Retrieves a specific todo
  • PUT /todos/{id} - Updates a todo
  • DELETE /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:

  1. Prefer runtimes that start quickly, like Node.js or Python
  2. Keep Lambda functions small and focused
  3. Use the Provisioned Concurrency feature
  4. Minimize external dependencies

DynamoDB Data Modeling#

For effective data modeling in DynamoDB:

  1. Determine your access patterns in advance
  2. Avoid over-normalization
  3. Store related data together
  4. Effectively use GSI and LSI indexes

Security Best Practices#

  1. Create IAM policies with the principle of least privilege
  2. Use AWS_IAM or Cognito authorization for API Gateway
  3. Store sensitive information in AWS Secrets Manager
  4. Consider running Lambda functions inside a VPC

Cost Optimization#

To optimize the costs of serverless applications:

  1. Adjust Lambda memory size according to the workload
  2. Prefer Auto Scaling or On-Demand capacity for DynamoDB
  3. Use API Gateway caching feature
  4. Accelerate static content delivery with CloudFront
  5. 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.

If you liked this article, you can share it on social media.
Enes Karakuş

Enes Karakuş

Microservice architecture and API systems expert. Develops cloud solutions with Kubernetes and Docker. Experienced in database and system architecture.

Get Support for Your Project

Get professional development services for modern web applications.

Contact Us