Building Production-Ready REST APIs with API Gateway and Lambda

AWS serverless architecture is the best platform to construct scalable REST APIs. This is a step-by-step guide that will detail the process of building production-ready API using Amazon API Gateway and AWS Lambda, including the initial setup process, as well as more complex security and performance optimization.

The Rationale behind Using Serverless with REST APIs?

Serverless APIs present a number of interesting benefits:

  • Cost Effectiveness: Pay but when needed, not wasted server bandwidth.
  • Auto-scaling: Notice traffic spikes automatically.
  • No Server Management: Business, not infrastructure.
  • High Availability: There is achieving of built-in redundancy between several AZs.
  • Fast Development: Short development and iteration times.

Architecture Overview

Our serverless API infrastructure comprises of:

  • API Gateway: This is where all the requests enter with inbuilt functions such as throttling, caching, and authentication.
  • Lambda Functions: Multiple runtime environments Lambda implementations Business logic execution.
  • DynamoDB: Serverless data persistence database.
  • IAM: Access control on a fine-grained basis.
  • CloudWatch: Logging and monitoring.

Quick Start: Minimal Working Example

⚡ New to serverless? Start here with this 5-minute minimal working example. You'll have a functioning API before diving into advanced features.

Let's build a simple "Hello World" API that you can deploy and test in minutes. This example demonstrates the core concepts without overwhelming complexity.

AWS Lambda Console - Accessing Lambda from AWS Management Console

Accessing AWS Lambda from the AWS Management Console - Search for "Lambda" in the services search bar to navigate to the Lambda dashboard where you can create and manage your functions.

Step 1: Create Lambda Function (app.py)

import json

def lambda_handler(event, context):
    """Simple Hello World API endpoint"""
    
    # Extract HTTP method and path
    method = event.get('httpMethod', 'GET')
    path = event.get('path', '/')
    
    # Simple routing
    if method == 'GET' and path == '/hello':
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({
                'message': 'Hello from serverless!',
                'timestamp': context.request_id
            })
        }
    
    # Default 404 response
    return {
        'statusCode': 404,
        'body': json.dumps({'error': 'Not Found'})
    }

Step 2: Deploy with AWS SAM (template.yaml)

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Minimal Serverless API Example

Resources:
  # Lambda function
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.lambda_handler
      Runtime: python3.11
      MemorySize: 128
      Timeout: 10
      Events:
        HelloWorldApi:
          Type: Api
          Properties:
            Path: /hello
            Method: GET

Outputs:
  ApiEndpoint:
    Description: "API Gateway endpoint URL"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello"

Step 3: Deploy

# Initialize SAM (first time only)
sam init

# Build and deploy
sam build
sam deploy --guided

# Test your API
curl https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/Prod/hello

Expected Response

{
  "message": "Hello from serverless!",
  "timestamp": "abc123-request-id"
}

You now have a working serverless API! Let's expand this into a production-ready application.

Complete End-to-End Working Example: User Management API

Now let's build a full CRUD API with DynamoDB integration, proper error handling, and security. This complete example is production-ready and demonstrates real-world patterns.

Project Structure

user-api/
├── app.py                  # Lambda function code
├── requirements.txt        # Python dependencies
├── template.yaml           # SAM template
└── terraform/
    ├── main.tf            # Terraform configuration
    ├── variables.tf
    └── outputs.tf

Complete Lambda Function (app.py)

import json
import boto3
import uuid
import os
from datetime import datetime
from boto3.dynamodb.conditions import Key

# Initialize DynamoDB
dynamodb = boto3.resource('dynamodb')
table_name = os.environ.get('TABLE_NAME', 'Users')
table = dynamodb.Table(table_name)

def lambda_handler(event, context):
    """Main handler routing requests to appropriate functions"""
    
    try:
        http_method = event['httpMethod']
        path = event['path']
        
        # Route based on HTTP method and path
        if http_method == 'GET' and path == '/users':
            return get_all_users()
        
        elif http_method == 'GET' and path.startswith('/users/'):
            user_id = path.split('/')[-1]
            return get_user(user_id)
        
        elif http_method == 'POST' and path == '/users':
            return create_user(event)
        
        elif http_method == 'PUT' and path.startswith('/users/'):
            user_id = path.split('/')[-1]
            return update_user(user_id, event)
        
        elif http_method == 'DELETE' and path.startswith('/users/'):
            user_id = path.split('/')[-1]
            return delete_user(user_id)
        
        else:
            return response(404, {'error': 'Route not found'})
    
    except Exception as e:
        print(f"Error: {str(e)}")
        return response(500, {'error': 'Internal server error', 'details': str(e)})

def get_all_users():
    """Retrieve all users from DynamoDB"""
    try:
        result = table.scan(Limit=100)
        
        return response(200, {
            'users': result.get('Items', []),
            'count': len(result.get('Items', []))
        })
    
    except Exception as e:
        return response(500, {'error': 'Failed to retrieve users', 'details': str(e)})

def get_user(user_id):
    """Get a single user by ID"""
    try:
        result = table.get_item(Key={'userId': user_id})
        
        if 'Item' not in result:
            return response(404, {'error': 'User not found'})
        
        return response(200, result['Item'])
    
    except Exception as e:
        return response(500, {'error': 'Failed to retrieve user', 'details': str(e)})

def create_user(event):
    """Create a new user"""
    try:
        # Parse request body
        body = json.loads(event.get('body', '{}'))
        
        # Validate required fields
        if not body.get('name') or not body.get('email'):
            return response(400, {'error': 'Name and email are required'})
        
        # Create user object
        user = {
            'userId': str(uuid.uuid4()),
            'name': body['name'],
            'email': body['email'],
            'createdAt': datetime.utcnow().isoformat(),
            'updatedAt': datetime.utcnow().isoformat()
        }
        
        # Optional fields
        if body.get('phone'):
            user['phone'] = body['phone']
        
        # Save to DynamoDB
        table.put_item(Item=user)
        
        return response(201, {
            'message': 'User created successfully',
            'user': user
        })
    
    except json.JSONDecodeError:
        return response(400, {'error': 'Invalid JSON in request body'})
    
    except Exception as e:
        return response(500, {'error': 'Failed to create user', 'details': str(e)})

def update_user(user_id, event):
    """Update existing user"""
    try:
        # Check if user exists
        existing = table.get_item(Key={'userId': user_id})
        if 'Item' not in existing:
            return response(404, {'error': 'User not found'})
        
        # Parse update data
        body = json.loads(event.get('body', '{}'))
        
        # Build update expression
        update_expr = "SET updatedAt = :updated"
        expr_values = {':updated': datetime.utcnow().isoformat()}
        
        if body.get('name'):
            update_expr += ", #n = :name"
            expr_values[':name'] = body['name']
        
        if body.get('email'):
            update_expr += ", email = :email"
            expr_values[':email'] = body['email']
        
        if body.get('phone'):
            update_expr += ", phone = :phone"
            expr_values[':phone'] = body['phone']
        
        # Perform update
        result = table.update_item(
            Key={'userId': user_id},
            UpdateExpression=update_expr,
            ExpressionAttributeNames={'#n': 'name'} if body.get('name') else None,
            ExpressionAttributeValues=expr_values,
            ReturnValues='ALL_NEW'
        )
        
        return response(200, {
            'message': 'User updated successfully',
            'user': result['Attributes']
        })
    
    except json.JSONDecodeError:
        return response(400, {'error': 'Invalid JSON in request body'})
    
    except Exception as e:
        return response(500, {'error': 'Failed to update user', 'details': str(e)})

def delete_user(user_id):
    """Delete a user"""
    try:
        # Check if user exists
        existing = table.get_item(Key={'userId': user_id})
        if 'Item' not in existing:
            return response(404, {'error': 'User not found'})
        
        # Delete the user
        table.delete_item(Key={'userId': user_id})
        
        return response(200, {'message': 'User deleted successfully'})
    
    except Exception as e:
        return response(500, {'error': 'Failed to delete user', 'details': str(e)})

def response(status_code, body):
    """Helper function to format API responses"""
    return {
        'statusCode': status_code,
        'headers': {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Headers': 'Content-Type,Authorization',
            'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS'
        },
        'body': json.dumps(body)
    }

Complete AWS SAM Template (template.yaml)

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Production-Ready User Management API

Globals:
  Function:
    Timeout: 30
    Runtime: python3.11
    MemorySize: 256
    Environment:
      Variables:
        TABLE_NAME: !Ref UsersTable
    Tracing: Active  # Enable X-Ray

Resources:
  # DynamoDB Table
  UsersTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: Users
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: userId
          AttributeType: S
      KeySchema:
        - AttributeName: userId
          KeyType: HASH
      StreamSpecification:
        StreamViewType: NEW_AND_OLD_IMAGES
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true
      Tags:
        - Key: Environment
          Value: Production

  # Lambda Function
  UserApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: UserManagementAPI
      CodeUri: .
      Handler: app.lambda_handler
      Description: User Management CRUD API
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref UsersTable
      Events:
        # GET /users - List all users
        GetUsers:
          Type: Api
          Properties:
            Path: /users
            Method: GET
            RestApiId: !Ref UserApi
        
        # GET /users/{id} - Get specific user
        GetUser:
          Type: Api
          Properties:
            Path: /users/{id}
            Method: GET
            RestApiId: !Ref UserApi
        
        # POST /users - Create user
        CreateUser:
          Type: Api
          Properties:
            Path: /users
            Method: POST
            RestApiId: !Ref UserApi
        
        # PUT /users/{id} - Update user
        UpdateUser:
          Type: Api
          Properties:
            Path: /users/{id}
            Method: PUT
            RestApiId: !Ref UserApi
        
        # DELETE /users/{id} - Delete user
        DeleteUser:
          Type: Api
          Properties:
            Path: /users/{id}
            Method: DELETE
            RestApiId: !Ref UserApi

  # API Gateway
  UserApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      TracingEnabled: true
      Cors:
        AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
        AllowHeaders: "'Content-Type,Authorization'"
        AllowOrigin: "'*'"
      ThrottleSettings:
        BurstLimit: 100
        RateLimit: 50
      MethodSettings:
        - ResourcePath: '/*'
          HttpMethod: '*'
          LoggingLevel: INFO
          DataTraceEnabled: true
          MetricsEnabled: true

  # CloudWatch Log Group
  ApiLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/lambda/${UserApiFunction}'
      RetentionInDays: 30

  # CloudWatch Alarm for Errors
  ApiErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: UserApiHighErrorRate
      AlarmDescription: Alert when API error rate exceeds threshold
      MetricName: Errors
      Namespace: AWS/Lambda
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 1
      Threshold: 10
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref UserApiFunction

Outputs:
  ApiEndpoint:
    Description: "API Gateway endpoint URL"
    Value: !Sub "https://${UserApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"
  
  UsersTableName:
    Description: "DynamoDB table name"
    Value: !Ref UsersTable
  
  FunctionArn:
    Description: "Lambda Function ARN"
    Value: !GetAtt UserApiFunction.Arn

Complete Terraform Configuration

terraform/main.tf

terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

# DynamoDB Table
resource "aws_dynamodb_table" "users" {
  name           = "Users"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "userId"

  attribute {
    name = "userId"
    type = "S"
  }

  stream_enabled   = true
  stream_view_type = "NEW_AND_OLD_IMAGES"

  point_in_time_recovery {
    enabled = true
  }

  tags = {
    Name        = "Users Table"
    Environment = "Production"
  }
}

# Lambda Execution Role
resource "aws_iam_role" "lambda_role" {
  name = "user-api-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}

# Lambda Policy for DynamoDB Access
resource "aws_iam_role_policy" "lambda_policy" {
  name = "user-api-lambda-policy"
  role = aws_iam_role.lambda_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "dynamodb:GetItem",
          "dynamodb:PutItem",
          "dynamodb:UpdateItem",
          "dynamodb:DeleteItem",
          "dynamodb:Scan",
          "dynamodb:Query"
        ]
        Resource = aws_dynamodb_table.users.arn
      },
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "arn:aws:logs:*:*:*"
      },
      {
        Effect = "Allow"
        Action = [
          "xray:PutTraceSegments",
          "xray:PutTelemetryRecords"
        ]
        Resource = "*"
      }
    ]
  })
}

# Package Lambda function
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_file = "${path.module}/../app.py"
  output_path = "${path.module}/lambda_function.zip"
}

# Lambda Function
resource "aws_lambda_function" "user_api" {
  filename         = data.archive_file.lambda_zip.output_path
  function_name    = "UserManagementAPI"
  role            = aws_iam_role.lambda_role.arn
  handler         = "app.lambda_handler"
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
  runtime         = "python3.11"
  memory_size     = 256
  timeout         = 30

  environment {
    variables = {
      TABLE_NAME = aws_dynamodb_table.users.name
    }
  }

  tracing_config {
    mode = "Active"
  }

  tags = {
    Name = "User API Lambda"
  }
}

# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "api_logs" {
  name              = "/aws/lambda/${aws_lambda_function.user_api.function_name}"
  retention_in_days = 30
}

# API Gateway REST API
resource "aws_api_gateway_rest_api" "user_api" {
  name        = "UserManagementAPI"
  description = "User Management REST API"

  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

# API Gateway Resource - /users
resource "aws_api_gateway_resource" "users" {
  rest_api_id = aws_api_gateway_rest_api.user_api.id
  parent_id   = aws_api_gateway_rest_api.user_api.root_resource_id
  path_part   = "users"
}

# API Gateway Resource - /users/{id}
resource "aws_api_gateway_resource" "user_id" {
  rest_api_id = aws_api_gateway_rest_api.user_api.id
  parent_id   = aws_api_gateway_resource.users.id
  path_part   = "{id}"
}

# Methods for /users
resource "aws_api_gateway_method" "get_users" {
  rest_api_id   = aws_api_gateway_rest_api.user_api.id
  resource_id   = aws_api_gateway_resource.users.id
  http_method   = "GET"
  authorization = "NONE"
}

resource "aws_api_gateway_method" "post_user" {
  rest_api_id   = aws_api_gateway_rest_api.user_api.id
  resource_id   = aws_api_gateway_resource.users.id
  http_method   = "POST"
  authorization = "NONE"
}

# Methods for /users/{id}
resource "aws_api_gateway_method" "get_user" {
  rest_api_id   = aws_api_gateway_rest_api.user_api.id
  resource_id   = aws_api_gateway_resource.user_id.id
  http_method   = "GET"
  authorization = "NONE"
}

resource "aws_api_gateway_method" "put_user" {
  rest_api_id   = aws_api_gateway_rest_api.user_api.id
  resource_id   = aws_api_gateway_resource.user_id.id
  http_method   = "PUT"
  authorization = "NONE"
}

resource "aws_api_gateway_method" "delete_user" {
  rest_api_id   = aws_api_gateway_rest_api.user_api.id
  resource_id   = aws_api_gateway_resource.user_id.id
  http_method   = "DELETE"
  authorization = "NONE"
}

# Lambda Integrations
resource "aws_api_gateway_integration" "get_users_integration" {
  rest_api_id = aws_api_gateway_rest_api.user_api.id
  resource_id = aws_api_gateway_resource.users.id
  http_method = aws_api_gateway_method.get_users.http_method

  integration_http_method = "POST"
  type                   = "AWS_PROXY"
  uri                    = aws_lambda_function.user_api.invoke_arn
}

resource "aws_api_gateway_integration" "post_user_integration" {
  rest_api_id = aws_api_gateway_rest_api.user_api.id
  resource_id = aws_api_gateway_resource.users.id
  http_method = aws_api_gateway_method.post_user.http_method

  integration_http_method = "POST"
  type                   = "AWS_PROXY"
  uri                    = aws_lambda_function.user_api.invoke_arn
}

resource "aws_api_gateway_integration" "get_user_integration" {
  rest_api_id = aws_api_gateway_rest_api.user_api.id
  resource_id = aws_api_gateway_resource.user_id.id
  http_method = aws_api_gateway_method.get_user.http_method

  integration_http_method = "POST"
  type                   = "AWS_PROXY"
  uri                    = aws_lambda_function.user_api.invoke_arn
}

resource "aws_api_gateway_integration" "put_user_integration" {
  rest_api_id = aws_api_gateway_rest_api.user_api.id
  resource_id = aws_api_gateway_resource.user_id.id
  http_method = aws_api_gateway_method.put_user.http_method

  integration_http_method = "POST"
  type                   = "AWS_PROXY"
  uri                    = aws_lambda_function.user_api.invoke_arn
}

resource "aws_api_gateway_integration" "delete_user_integration" {
  rest_api_id = aws_api_gateway_rest_api.user_api.id
  resource_id = aws_api_gateway_resource.user_id.id
  http_method = aws_api_gateway_method.delete_user.http_method

  integration_http_method = "POST"
  type                   = "AWS_PROXY"
  uri                    = aws_lambda_function.user_api.invoke_arn
}

# Lambda Permissions for API Gateway
resource "aws_lambda_permission" "api_gateway" {
  statement_id  = "AllowAPIGatewayInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.user_api.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.user_api.execution_arn}/*/*"
}

# API Gateway Deployment
resource "aws_api_gateway_deployment" "api_deployment" {
  depends_on = [
    aws_api_gateway_integration.get_users_integration,
    aws_api_gateway_integration.post_user_integration,
    aws_api_gateway_integration.get_user_integration,
    aws_api_gateway_integration.put_user_integration,
    aws_api_gateway_integration.delete_user_integration
  ]

  rest_api_id = aws_api_gateway_rest_api.user_api.id
  stage_name  = "prod"

  lifecycle {
    create_before_destroy = true
  }
}

# API Gateway Stage Settings
resource "aws_api_gateway_stage" "prod" {
  deployment_id = aws_api_gateway_deployment.api_deployment.id
  rest_api_id   = aws_api_gateway_rest_api.user_api.id
  stage_name    = "prod"

  xray_tracing_enabled = true

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.api_gateway_logs.arn
    format = jsonencode({
      requestId      = "$context.requestId"
      ip             = "$context.identity.sourceIp"
      caller         = "$context.identity.caller"
      user           = "$context.identity.user"
      requestTime    = "$context.requestTime"
      httpMethod     = "$context.httpMethod"
      resourcePath   = "$context.resourcePath"
      status         = "$context.status"
      protocol       = "$context.protocol"
      responseLength = "$context.responseLength"
    })
  }
}

# CloudWatch Log Group for API Gateway
resource "aws_cloudwatch_log_group" "api_gateway_logs" {
  name              = "/aws/apigateway/${aws_api_gateway_rest_api.user_api.name}"
  retention_in_days = 30
}

# CloudWatch Alarm
resource "aws_cloudwatch_metric_alarm" "api_errors" {
  alarm_name          = "UserApiHighErrorRate"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "1"
  metric_name         = "Errors"
  namespace           = "AWS/Lambda"
  period              = "300"
  statistic           = "Sum"
  threshold           = "10"
  alarm_description   = "Alert when API error rate exceeds threshold"

  dimensions = {
    FunctionName = aws_lambda_function.user_api.function_name
  }
}

terraform/variables.tf

variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-east-1"
}

terraform/outputs.tf

output "api_endpoint" {
  description = "API Gateway endpoint URL"
  value       = "${aws_api_gateway_stage.prod.invoke_url}/users"
}

output "dynamodb_table_name" {
  description = "DynamoDB table name"
  value       = aws_dynamodb_table.users.name
}

output "lambda_function_name" {
  description = "Lambda function name"
  value       = aws_lambda_function.user_api.function_name
}

Deployment Instructions

Option 1: Deploy with AWS SAM

# Install dependencies
pip install boto3 -t .

# Build and deploy
sam build
sam deploy --guided

# Follow prompts:
# - Stack Name: user-management-api
# - AWS Region: us-east-1
# - Confirm changes: Y
# - Allow SAM CLI IAM role creation: Y
# - Save arguments to config file: Y

Option 2: Deploy with Terraform

# Navigate to terraform directory
cd terraform

# Initialize Terraform
terraform init

# Review planned changes
terraform plan

# Apply configuration
terraform apply

# Type 'yes' to confirm

Testing Your API

1. Create a User

curl -X POST https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/Prod/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "john@example.com",
    "phone": "+1234567890"
  }'

# Response:
{
  "message": "User created successfully",
  "user": {
    "userId": "abc-123-def",
    "name": "John Doe",
    "email": "john@example.com",
    "phone": "+1234567890",
    "createdAt": "2026-01-08T10:30:00.000Z",
    "updatedAt": "2026-01-08T10:30:00.000Z"
  }
}

2. Get All Users

curl https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/Prod/users

# Response:
{
  "users": [
    {
      "userId": "abc-123-def",
      "name": "John Doe",
      "email": "john@example.com",
      "phone": "+1234567890",
      "createdAt": "2026-01-08T10:30:00.000Z"
    }
  ],
  "count": 1
}

3. Get Specific User

curl https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/Prod/users/abc-123-def

# Response:
{
  "userId": "abc-123-def",
  "name": "John Doe",
  "email": "john@example.com",
  "phone": "+1234567890",
  "createdAt": "2026-01-08T10:30:00.000Z",
  "updatedAt": "2026-01-08T10:30:00.000Z"
}

4. Update User

curl -X PUT https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/Prod/users/abc-123-def \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jane Doe",
    "phone": "+9876543210"
  }'

# Response:
{
  "message": "User updated successfully",
  "user": {
    "userId": "abc-123-def",
    "name": "Jane Doe",
    "email": "john@example.com",
    "phone": "+9876543210",
    "updatedAt": "2026-01-08T11:00:00.000Z"
  }
}

5. Delete User

curl -X DELETE https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/Prod/users/abc-123-def

# Response:
{
  "message": "User deleted successfully"
}

Testing with Postman

  1. Add a new collection that is called User Management API.
  2. Please add your API endpoint as the variable: {{baseUrl}} = https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/Prod
  3. Make endpoint requests:
    • GET {{baseUrl}}/users
    • POST {{baseUrl}}/users with JSON body
    • GET {{baseUrl}}/users/:userId
    • PUT {{baseUrl}}/users/:userId with JSON body
    • DELETE {{baseUrl}}/users/:userId
  4. Store POST response stream userid and use in later requests.
  5. Run check to ensure that all terminals are tested.

A full production-ready, serverless server has now been made! There are also all CRUD operations, adequate integration with errors, with DynamoDB and may be deployed either using SAM or Terraform.

Security Implementation at the Advance Level

API Key and Rate Limiting

It is important to protect your API. The API Gateway has inbuilt capabilities of:

  • API Keys: Use Identity and Tracking of API Consumers.
  • pression Profiles: Have limits on which to use and to limit usage per client.
  • Rate Limiting: Avert the abuse by setting up request limits.

An average economic configuration may accommodate 1,000 concurrent requests per second and 2,000 for bursting alongside 100,000 requests per API-key every month.

JWT Token Validation

To perform authentication of users, authenticate your Lambda function by using the validation of JWT tokens:

def validate_token(event):
    token = event['headers'].get('Authorization', '').replace('Bearer ', '')
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        return payload
    except jwt.ExpiredSignatureError:
        return None

This ensures only authenticated users can access protected endpoints.

Performance Optimization Strategies

Key Performance Tips:

  • Connection Pooling: Reuse database connections across Lambda invocations to reduce latency
  • Provisioned Concurrency: Keep functions warm for critical endpoints to eliminate cold starts
  • Right-size Memory: Balance cost and performance by choosing optimal Lambda memory allocation
  • API Gateway Caching: Cache frequent GET requests to reduce backend load
  • Efficient Queries: Use DynamoDB indexes and query patterns wisely

A well-optimized serverless API can handle thousands of requests per second with sub-second response times while keeping costs minimal.

Monitoring and Observability

AWS will give you the potent tools to monitor your serverless APIs:

CloudWatch Metrics and Alarms

  • Error Rates: Track 4XX and 5XX errors to identify problems at an early stage.
  • Latency: Check the response to the users to make sure it is good.
  • Throttling: Warn about hitting rate limits.
  • Cost Anomalies: Identify unusual peaks of utilization.

X-Ray Tracing

Enhance AWS X-Ray to trace the path of requests to your serverless architecture. This can be used to determine the bottlenecks and to optimize your entire application stack.

Deployment and CI/CD

Speed up, increase reliability: Automate your deployment process:

Deployment Best Practices

  • Infrastructure as Code: AWS SAM, CloudFormation or Terraform.
  • Automated Testing: Conduct unit and integration testing prior to deployment.
  • Staged Rollouts: Stage to dev/staging environments.
  • Rollback Strategy: Store the past versions to roll them back in case of necessity.

The whole process of code commitment to production deployment can be automated with the help of such tools as AWS CodePipeline, GitHub Actions, or GitLab CI/CD.

Optimizing Costs Best Practices

  • Optimise Lambda memory: tuning of AWS Lambda Power can be used to retrieve optimal settings.
  • Apply caching: Save responses at API Gateway level when doing data that is shared.
  • Pay Per Use: Use DynamoDB On-Demand.
  • Monitor and alert: Install billing alarm and cost monitoring.
  • Minimize cold starts: Only use provisioned concurrency when necessary.

Advanced Best Practices to API Production

These considerations will go beyond the base to enable you to create serverless APIs that are real production-grade and secure, performant and cost-optimized.

A comparison between the HTTP APIs and the REST APIs: How to make the correct choice?

There are two types of API that AWS API Gateway provides, and a correct choice is important to influence your costs, performance, and features available:

HTTP APIs ( API Gateway v2 )- It is the preferred choice in the majority of cases

There is a more recent and simpler API, called HTTP API, that is suitable to use in modern serverless applications:

  • 70% Reduced Price: HTTP APIs are half the price of APIs of REST, i.e. 1.00 million requests would cost 3.50 USD.
  • Less Latency: Optimized architecture would provide faster response times of 50-100ms on average.
  • Native JWT Support: In-the-box JWT authorization without user-defined Lambda authorizers is simpler and less expensive.
  • Automatic CORS Operating: Single line setups of CORS.
  • WebSocket Support: Allows real-time two-way communication to use in chat and notifications and live updates.

REST APIs ( API Gateway v1) - Advanced Features

REST APIs offer full support of complex business needs:

  • API Key Management: API Key identification and rate limiting API keys with plans.
  • Request Validation: Before the Lambda functions are invoked, schema-based validation should be performed in the gateway.
  • Request/Response Transformation: Alter payloads with Velocity Template Language (VTL).
  • Edge Optimized Endpoints: CloudFront distribution to access the world in low latency.
  • VPC endpoint integration: Integrate secure internal services.
  • AWS WAF Integration: Direct Web Application Firewall against well known exploits.

Decision Framework

Choose HTTP APIs when:
✓ Building new APIs from scratch
✓ Cost optimization is a priority  
✓ Using OAuth 2.0 or JWT authentication
✓ Need WebSocket support for real-time features
✓ Simple proxy integrations are sufficient

Choose REST APIs when:
✓ Require API key management and usage plans
✓ Need request/response transformations via VTL
✓ Must integrate AWS WAF for security
✓ Require private API endpoints within VPC
✓ Need detailed per-method cache control

Intelligent Caching Strategy

Caching dramatically reduces backend load, improves response times, and lowers costs. Implement a multi-layered caching approach:

API Gateway Cache Configuration

Enable REST API caching to store responses for frequently requested data:

Resources:
  ApiGatewayStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      RestApiId: !Ref RestApi
      StageName: prod
      CacheClusterEnabled: true
      CacheClusterSize: '0.5'  # Options: 0.5GB to 237GB
      MethodSettings:
        - ResourcePath: /users
          HttpMethod: GET
          CachingEnabled: true
          CacheTtlInSeconds: 300  # 5 minutes
          CacheDataEncrypted: true
          
        - ResourcePath: /products
          HttpMethod: GET
          CachingEnabled: true
          CacheTtlInSeconds: 3600  # 1 hour for stable data

Cache-Control Headers from Lambda

Control client-side and CDN caching behavior with appropriate headers:

def lambda_handler(event, context):
    # Static/public data - aggressive caching
    if event['path'] == '/api/public/products':
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Cache-Control': 'public, max-age=3600, s-maxage=7200',
                'ETag': generate_etag(data)
            },
            'body': json.dumps(products)
        }
    
    # User-specific data - private caching only
    if event['path'] == '/api/user/profile':
        return {
            'statusCode': 200,
            'headers': {
                'Cache-Control': 'private, max-age=300',
                'Vary': 'Authorization'
            },
            'body': json.dumps(user_data)
        }
    
    # Frequently changing data - minimal caching
    if event['path'] == '/api/live/stock-prices':
        return {
            'statusCode': 200,
            'headers': {
                'Cache-Control': 'no-cache, must-revalidate, max-age=0'
            },
            'body': json.dumps(live_data)
        }

Application-Level Caching

Implement in-memory caching within Lambda for warm container reuse:

import time
from functools import lru_cache

# Global cache persists across warm invocations
cache_store = {}
CACHE_TTL = 300  # 5 minutes

def get_cached_data(key):
    """Retrieve data with TTL-based cache"""
    if key in cache_store:
        data, timestamp = cache_store[key]
        if time.time() - timestamp < CACHE_TTL:
            print(f"Cache hit for key: {key}")
            return data
    
    # Cache miss - fetch fresh data
    data = fetch_from_database(key)
    cache_store[key] = (data, time.time())
    return data

# LRU cache for expensive computations
@lru_cache(maxsize=256)
def calculate_recommendations(user_id, category):
    """Expensive calculation cached in memory"""
    return complex_ml_computation(user_id, category)

Caching Best Practices

  • Cache Only GET Requests: POST, PUT, DELETE should never be cached
  • Set Appropriate TTLs: Balance data freshness with cache efficiency (5 min for dynamic, 1 hour for stable)
  • Use Cache Keys Wisely: Include query parameters in cache keys for GET requests
  • Monitor Cache Hit Rates: Target 70%+ hit rate; adjust TTL or strategy if lower
  • Implement Cache Invalidation: Purge specific cache entries when data updates occur

Timeout and Payload Size Considerations

Understanding and properly configuring limits prevents unexpected failures and ensures reliable operation:

Timeout Configuration Hierarchy

# API Gateway maximum timeout: 29 seconds (hard limit)
# Configure Lambda timeout slightly lower for graceful handling

Resources:
  UserFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.handler
      Runtime: python3.11
      Timeout: 28  # Leave 1 second buffer for API Gateway
      MemorySize: 1024

Lambda Timeout Best Practices

  • API Endpoints: 3-15 seconds typical; keep synchronous operations fast
  • Background Processing: Use asynchronous invocation or Step Functions for long-running tasks
  • Database Queries: Set connection and query timeouts lower than Lambda timeout
  • External API Calls: Implement aggressive timeouts (3-5 seconds) with retry logic
import boto3
from botocore.config import Config

# Configure AWS SDK with proper timeouts
config = Config(
    connect_timeout=3,
    read_timeout=5,
    retries={
        'max_attempts': 2,
        'mode': 'standard'
    }
)

dynamodb = boto3.resource('dynamodb', config=config)

# HTTP client timeout configuration
import requests

def call_external_api(url):
    try:
        response = requests.get(
            url,
            timeout=(3, 5),  # (connect, read) timeouts
            headers={'User-Agent': 'MyAPI/1.0'}
        )
        return response.json()
    except requests.Timeout:
        # Fallback or error handling
        return {'error': 'External service timeout'}

Payload Size Limits

AWS imposes specific size constraints across services:

  • API Gateway Request: 10 MB maximum payload size
  • API Gateway Response: 10 MB maximum (6 MB practical due to base64 encoding)
  • Lambda Synchronous: 6 MB request and response payload
  • Lambda Asynchronous: 256 KB for event payload
  • DynamoDB Item: 400 KB maximum item size

Handling Large Payloads

For files or data exceeding limits, use presigned S3 URLs:

import boto3
import uuid

s3_client = boto3.client('s3')

def upload_large_file(event, context):
    """Generate presigned URL for direct S3 upload"""
    
    file_key = f"uploads/{uuid.uuid4()}.pdf"
    
    # Generate presigned POST URL
    presigned_post = s3_client.generate_presigned_post(
        Bucket='my-upload-bucket',
        Key=file_key,
        Fields={'acl': 'private'},
        Conditions=[
            {'acl': 'private'},
            ['content-length-range', 0, 104857600]  # Max 100MB
        ],
        ExpiresIn=3600  # 1 hour validity
    )
    
    return {
        'statusCode': 200,
        'body': json.dumps({
            'uploadUrl': presigned_post['url'],
            'fields': presigned_post['fields'],
            'fileKey': file_key
        })
    }

def download_large_file(event, context):
    """Generate presigned URL for direct S3 download"""
    
    file_key = event['pathParameters']['fileKey']
    
    presigned_url = s3_client.generate_presigned_url(
        'get_object',
        Params={
            'Bucket': 'my-download-bucket',
            'Key': file_key
        },
        ExpiresIn=3600
    )
    
    return {
        'statusCode': 200,
        'body': json.dumps({'downloadUrl': presigned_url})
    }

Request Size Validation

def lambda_handler(event, context):
    """Validate payload size early"""
    
    body = event.get('body', '')
    
    # Check size (API Gateway base64 encodes binary data)
    if event.get('isBase64Encoded'):
        # Approximate original size
        size_bytes = len(body) * 0.75
    else:
        size_bytes = len(body)
    
    if size_bytes > 5 * 1024 * 1024:  # 5 MB threshold
        return {
            'statusCode': 413,  # Payload Too Large
            'body': json.dumps({
                'error': 'Request payload exceeds 5MB limit',
                'message': 'Use presigned S3 URL for large files'
            })
        }
    
    # Process request
    return process_data(json.loads(body))

Comprehensive Security Implementation

Security must be layered across multiple dimensions to protect your APIs from threats:

AWS WAF Integration for DDoS and Attack Prevention

AWS Web Application Firewall provides protection against common web exploits and volumetric attacks:

Resources:
  # Create Web ACL with managed rule groups
  ApiWebACL:
    Type: AWS::WAFv2::WebACL
    Properties:
      Scope: REGIONAL
      DefaultAction:
        Allow: {}
      Rules:
        # Rate limiting per IP
        - Name: RateLimitRule
          Priority: 1
          Statement:
            RateBasedStatement:
              Limit: 2000  # 2000 requests per 5 minutes per IP
              AggregateKeyType: IP
          Action:
            Block:
              CustomResponse:
                ResponseCode: 429
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: RateLimitRule
        
        # AWS Managed Rules - Core Rule Set
        - Name: AWSManagedRulesCommonRuleSet
          Priority: 2
          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesCommonRuleSet
          OverrideAction:
            None: {}
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: CommonRuleSet
        
        # SQL Injection protection
        - Name: SQLiProtection
          Priority: 3
          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesSQLiRuleSet
          OverrideAction:
            None: {}
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: SQLiProtection
        
        # Known bad inputs protection
        - Name: KnownBadInputsRuleSet
          Priority: 4
          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesKnownBadInputsRuleSet
          OverrideAction:
            None: {}
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: KnownBadInputs
        
        # Geographic blocking (optional)
        - Name: GeoBlockRule
          Priority: 5
          Statement:
            GeoMatchStatement:
              CountryCodes:
                - CN  # Block specific countries if needed
                - RU
          Action:
            Block: {}
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: GeoBlockRule
      
      VisibilityConfig:
        SampledRequestsEnabled: true
        CloudWatchMetricsEnabled: true
        MetricName: ApiWebACL

  # Associate WAF with API Gateway
  WebACLAssociation:
    Type: AWS::WAFv2::WebACLAssociation
    Properties:
      ResourceArn: !Sub 'arn:aws:apigateway:${AWS::Region}::/restapis/${RestApi}/stages/prod'
      WebACLArn: !GetAtt ApiWebACL.Arn

API Keys and Usage Plans

Implement client identification and rate limiting with API keys:

Resources:
  # Create usage plan with throttling
  ApiUsagePlan:
    Type: AWS::ApiGateway::UsagePlan
    Properties:
      UsagePlanName: StandardPlan
      Description: Standard API access with rate limits
      Throttle:
        BurstLimit: 100  # Max concurrent requests
        RateLimit: 50    # Sustained requests per second
      Quota:
        Limit: 10000     # Total requests per period
        Period: DAY
      ApiStages:
        - ApiId: !Ref RestApi
          Stage: !Ref ProdStage

  # Create API key
  ApiKey:
    Type: AWS::ApiGateway::ApiKey
    Properties:
      Name: ClientApiKey
      Description: API key for external client
      Enabled: true

  # Associate key with usage plan
  UsagePlanKey:
    Type: AWS::ApiGateway::UsagePlanKey
    Properties:
      KeyId: !Ref ApiKey
      KeyType: API_KEY
      UsagePlanId: !Ref ApiUsagePlan

Validate API keys in your Lambda functions for additional security:

def lambda_handler(event, context):
    """Validate API key and client permissions"""
    
    api_key = event['headers'].get('x-api-key')
    
    if not api_key:
        return {
            'statusCode': 401,
            'body': json.dumps({'error': 'API key required'})
        }
    
    # Validate key against DynamoDB or Secrets Manager
    if not validate_api_key(api_key):
        return {
            'statusCode': 403,
            'body': json.dumps({'error': 'Invalid API key'})
        }
    
    # Process authorized request
    return process_request(event)

Amazon Cognito Integration for User Authentication

Implement secure user authentication with Amazon Cognito User Pools:

Resources:
  # Create Cognito User Pool
  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: ApiUserPool
      AutoVerifiedAttributes:
        - email
      Schema:
        - Name: email
          Required: true
          Mutable: false
      Policies:
        PasswordPolicy:
          MinimumLength: 12
          RequireUppercase: true
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true

  # User Pool Client
  UserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: ApiClient
      UserPoolId: !Ref UserPool
      GenerateSecret: false
      ExplicitAuthFlows:
        - ALLOW_USER_PASSWORD_AUTH
        - ALLOW_REFRESH_TOKEN_AUTH

  # API Gateway Authorizer
  CognitoAuthorizer:
    Type: AWS::ApiGateway::Authorizer
    Properties:
      Name: CognitoAuthorizer
      Type: COGNITO_USER_POOLS
      IdentitySource: method.request.header.Authorization
      RestApiId: !Ref RestApi
      ProviderARNs:
        - !GetAtt UserPool.Arn

Access authenticated user information in Lambda:

def lambda_handler(event, context):
    """Access Cognito user claims"""
    
    # Extract user identity from Cognito authorizer
    claims = event['requestContext']['authorizer']['claims']
    
    user_id = claims['sub']  # Cognito user ID
    email = claims['email']
    username = claims['cognito:username']
    
    # Implement user-specific logic
    user_data = get_user_data(user_id)
    
    return {
        'statusCode': 200,
        'body': json.dumps({
            'user': email,
            'data': user_data
        })
    }

The Checklist Security Best Practices

  • API Gateway: The default is TLS 1.2+ only.
  • AWS WAF: Top 10 vulnerabilities protection.
  • Turn on Rate Limiting: throttling and burst limits nateate There is no throttling problem with either throttling or burst limits.
  • Validate Input: Sanitize of all user inputs at gateway and Lambda.
  • Least Privilege IAM: Minimal permissions to Lambda execution roles
  • Keep Data Secret: Encrypt Sensitive Data with AWS KMS.
  • Enable Access Logging: Enable logging of all API requests in CloudWatch or S3.
  • Secrets Manager: Always remove secrets and API keys
  • CORS: Only trust domains.
  • Check Security Events: Configure CloudWatch alarms of suspicious activity.

Conclusion

REST API Construction Production-Ready API Gateway and Lambda is a scalable and cost-effective solution to build the modern application. With the right security controls, optimization of performance and monitoring, it is possible to deploy APIs, which serve millions of requests with low latency and high availability.

The serverless model removes infrastructure maintenance burden meaning that you can focus on business value creation. Patterns and best practices on creating sound serverless APIs under the patterns and best practices discussed in this guide have you prepared to create serverless APIs that can easily scale as your application grows.

Next Steps: Try implementing these patterns in your own projects, and consider extending the API with additional features like WebSocket support using API Gateway v2, or implementing GraphQL endpoints for more flexible data fetching.