AWS Lambda Function Development

Module 26: Advanced Backend & API Development

Introduction to AWS Lambda

AWS Lambda is Amazon's Function-as-a-Service (FaaS) offering that lets you run code without provisioning or managing servers. It executes your code only when needed and scales automatically, from a few requests per day to thousands per second.

Analogy: Lambda as an On-Demand Kitchen

Traditional server hosting is like owning a restaurant:

  • You pay for the space, equipment, and staff whether customers are there or not
  • Limited by kitchen capacity during peak hours
  • You're responsible for maintenance, cleanliness, and upgrades

AWS Lambda is like a food delivery service with on-demand chefs:

  • Chefs (functions) only arrive when an order (event) comes in
  • You pay only when food is being prepared
  • More chefs automatically appear when multiple orders arrive
  • All equipment, space, and maintenance handled by the service
  • Each order prepared in isolation without affecting others

Key Lambda Concepts

graph TD A[AWS Lambda] --> B[Functions] A --> C[Triggers] A --> D[Execution Environment] A --> E[Permissions] B --> B1[Handler] B --> B2[Runtime] B --> B3[Memory/CPU] B --> B4[Timeout] C --> C1[API Gateway] C --> C2[S3 Events] C --> C3[DynamoDB Streams] C --> C4[CloudWatch Events] C --> C5[SNS/SQS] D --> D1[Initialization Phase] D --> D2[Invocation Phase] D --> D3[Execution Context] E --> E1[IAM Roles] E --> E2[Resource Policies]

Creating Your First Lambda Function

Let's start by creating a simple AWS Lambda function:

Function Structure

A Lambda function consists of a handler function that processes events:


// Basic Lambda function structure in Node.js
exports.handler = async (event, context) => {
  // Log the event object for debugging
  console.log('Event:', JSON.stringify(event, null, 2));
  console.log('Context:', JSON.stringify(context, null, 2));
  
  // Process the event
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Hello from Lambda!',
      input: event,
    }),
  };
  
  return response;
};
      

Lambda Handler in Different Languages

Python:


# Basic Lambda function in Python
def lambda_handler(event, context):
    # Log the event object for debugging
    print('Event:', event)
    print('Context:', context)
    
    # Process the event
    response = {
        'statusCode': 200,
        'body': {
            'message': 'Hello from Lambda!',
            'input': event
        }
    }
    
    return response
        

Java:


// Basic Lambda function in Java
package example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import java.util.HashMap;
import java.util.Map;

public class Handler implements RequestHandler<Map<String, Object>, Map<String, Object>> {
    @Override
    public Map<String, Object> handleRequest(Map<String, Object> event, Context context) {
        LambdaLogger logger = context.getLogger();
        logger.log("Event: " + event);
        
        Map<String, Object> body = new HashMap<>();
        body.put("message", "Hello from Lambda!");
        body.put("input", event);
        
        Map<String, Object> response = new HashMap<>();
        response.put("statusCode", 200);
        response.put("body", body);
        
        return response;
    }
}
        

Creating Lambda via AWS Console

The AWS Management Console provides a visual interface for creating Lambda functions:

  1. Log in to the AWS Management Console
  2. Navigate to the Lambda service
  3. Click "Create function"
  4. Choose "Author from scratch", provide a name, and select a runtime
  5. Create or select an execution role with appropriate permissions
  6. Configure basic settings (memory, timeout, etc.)
  7. Write or upload your function code
  8. Configure triggers and destinations
  9. Click "Create function"
AWS Lambda - Create function Basic information Function name: my-first-lambda Runtime Node.js 14.x Python 3.9 Java 11 Permissions Execution role: Create a new role with basic Lambda permissions Create function

Creating Lambda with AWS CLI

You can also create Lambda functions using the AWS Command Line Interface (CLI):


# Create a deployment package (ZIP file)
zip function.zip index.js

# Create a Lambda function
aws lambda create-function \
  --function-name my-lambda-function \
  --runtime nodejs14.x \
  --role arn:aws:iam::123456789012:role/lambda-execution-role \
  --handler index.handler \
  --zip-file fileb://function.zip \
  --timeout 10 \
  --memory-size 128
      

Understanding the Lambda Execution Environment

To effectively develop Lambda functions, it's important to understand how the execution environment works:

Initialization and Invocation Phases

Lambda functions go through two distinct phases during execution:

sequenceDiagram participant Client participant Lambda participant Function Note over Lambda,Function: Cold Start Client->>Lambda: Invoke Function Lambda->>Function: Initialize Execution Environment Note right of Function: Import dependencies Note right of Function: Initialize global variables Note right of Function: Connect to databases Function->>Function: Execute Handler Function->>Lambda: Return Response Lambda->>Client: Return Result Note over Lambda,Function: Warm Invocation Client->>Lambda: Invoke Function Again Note right of Function: Reuse Existing Environment Function->>Function: Execute Handler Function->>Lambda: Return Response Lambda->>Client: Return Result

Execution Context Reuse


// Global variables defined outside the handler are initialized once
// and can be reused across invocations in the same execution context
const AWS = require('aws-sdk');
const dynamoDB = new AWS.DynamoDB.DocumentClient();
let connectionPool;

// Expensive initialization should be done outside the handler
connectionPool = initializeConnectionPool();

exports.handler = async (event, context) => {
  console.log('Remaining time:', context.getRemainingTimeInMillis());
  console.log('Function name:', context.functionName);
  console.log('AWS request ID:', context.awsRequestId);
  
  // Use the connection pool from the global scope
  const result = await connectionPool.query('SELECT * FROM users');
  
  return {
    statusCode: 200,
    body: JSON.stringify(result)
  };
};

function initializeConnectionPool() {
  console.log('Initializing connection pool - this happens only during cold start');
  // Expensive initialization logic here
  return {
    query: async (sql) => {
      console.log(`Executing query: ${sql}`);
      return { items: [] }; // Simulated response
    }
  };
}
        

Memory and Performance

Lambda's CPU power is proportional to the amount of memory allocated, affecting both performance and cost:

Memory vs Performance and Cost

Memory (MB) Relative CPU Power Approx. Price per 1M Executions (1 sec) Best For
128 MB Baseline $0.21 Simple API handlers, minimal processing
512 MB 4x $0.84 Basic data transformations, light processing
1024 MB 8x $1.68 Image processing, moderate workloads
3008 MB 24x $5.01 Heavy computation, ML inference
10240 MB 82x $17.00 Intensive processing, fastest execution

Analogy: Choosing a Vehicle

Selecting Lambda memory/CPU is like choosing a vehicle for delivery:

  • 128 MB: Like a bicycle - cheap but slow, fine for small deliveries
  • 512 MB: Like a scooter - faster but still economical
  • 1024 MB: Like a compact car - good balance of speed and cost
  • 3008 MB: Like a delivery van - powerful and efficient for larger loads
  • 10240 MB: Like a truck - expensive but handles the heaviest workloads quickly

The right choice depends on your workload, budget, and performance requirements.

Execution Timeout

Lambda functions have a configurable timeout (up to 15 minutes) after which they are terminated:


// Handling potential timeouts
exports.handler = async (event, context) => {
  // Calculate time thresholds
  const startTime = Date.now();
  const timeoutThreshold = context.getRemainingTimeInMillis() - 1000; // 1 second buffer
  
  try {
    // Start processing a batch of items
    const items = event.items || [];
    const results = [];
    
    for (const item of items) {
      // Check remaining time before processing each item
      const elapsedTime = Date.now() - startTime;
      const remainingTime = timeoutThreshold - elapsedTime;
      
      if (remainingTime <= 0) {
        // Not enough time to process next item, return partial results
        console.log('Time running out, returning partial results');
        return {
          statusCode: 206, // Partial Content
          body: JSON.stringify({
            message: 'Partial processing due to timeout',
            processed: results.length,
            total: items.length,
            results: results
          })
        };
      }
      
      // Process the item
      console.log(`Processing item ${item.id}, remaining time: ${remainingTime}ms`);
      const result = await processItem(item);
      results.push(result);
    }
    
    return {
      statusCode: 200,
      body: JSON.stringify({
        message: 'All items processed successfully',
        results: results
      })
    };
  } catch (error) {
    console.error('Error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({
        message: 'Error processing items',
        error: error.message
      })
    };
  }
};

async function processItem(item) {
  // Simulate processing time
  await new Promise(resolve => setTimeout(resolve, 500));
  return { id: item.id, status: 'processed' };
}
      

Event Sources and Triggers

Lambda functions can be triggered by various event sources in AWS:

Synchronous Invocation

In synchronous invocation, the caller waits for the function to process the event and return a response:

API Gateway Integration

sequenceDiagram participant Client participant API Gateway participant Lambda Client->>API Gateway: HTTP Request API Gateway->>Lambda: Invoke Function Lambda->>Lambda: Process Request Lambda-->>API Gateway: HTTP Response API Gateway-->>Client: HTTP Response

// Lambda function for API Gateway
exports.handler = async (event) => {
  console.log('Event:', JSON.stringify(event, null, 2));
  
  // Extract path parameters
  const pathParameters = event.pathParameters || {};
  const id = pathParameters.id;
  
  // Extract query string parameters
  const queryStringParameters = event.queryStringParameters || {};
  const filter = queryStringParameters.filter;
  
  // Extract HTTP method
  const httpMethod = event.httpMethod;
  
  // Extract body for POST/PUT requests
  let body = {};
  if (event.body) {
    try {
      body = JSON.parse(event.body);
    } catch (error) {
      return {
        statusCode: 400,
        body: JSON.stringify({ message: 'Invalid request body' })
      };
    }
  }
  
  // Process based on HTTP method
  let response;
  switch (httpMethod) {
    case 'GET':
      if (id) {
        response = await getItem(id);
      } else {
        response = await listItems(filter);
      }
      break;
    case 'POST':
      response = await createItem(body);
      break;
    case 'PUT':
      response = await updateItem(id, body);
      break;
    case 'DELETE':
      response = await deleteItem(id);
      break;
    default:
      return {
        statusCode: 405,
        body: JSON.stringify({ message: 'Method not allowed' })
      };
  }
  
  return {
    statusCode: response.statusCode,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*' // CORS support
    },
    body: JSON.stringify(response.body)
  };
};

async function getItem(id) {
  // Implementation to fetch an item
  return { statusCode: 200, body: { id, name: 'Example Item' } };
}

// Other CRUD function implementations
        

Asynchronous Invocation

In asynchronous invocation, events are queued for processing and the Lambda service manages retries:

S3 Event Processing


// Lambda function for processing S3 events
exports.handler = async (event) => {
  console.log('Event:', JSON.stringify(event, null, 2));
  
  // Extract S3 event information
  const s3Event = event.Records[0].s3;
  const bucket = s3Event.bucket.name;
  const key = decodeURIComponent(s3Event.object.key.replace(/\+/g, ' '));
  const size = s3Event.object.size;
  
  console.log(`Processing new file: ${key} (${size} bytes) in bucket: ${bucket}`);
  
  try {
    // Process the file
    if (key.endsWith('.jpg') || key.endsWith('.png')) {
      await processImage(bucket, key);
    } else if (key.endsWith('.csv')) {
      await processCSV(bucket, key);
    } else if (key.endsWith('.pdf')) {
      await processPDF(bucket, key);
    } else {
      console.log(`Unsupported file type: ${key}`);
    }
    
    console.log(`Successfully processed: ${key}`);
    return { status: 'success', key };
  } catch (error) {
    console.error(`Error processing ${key}:`, error);
    throw error; // For asynchronous invocation, this triggers retry
  }
};

async function processImage(bucket, key) {
  // Implementation for image processing
  console.log(`Processing image: ${key}`);
  // Example: resize image, extract metadata, etc.
}

// Other file processing implementations
        

Stream-Based Invocation

For stream-based invocation, Lambda polls the stream and invokes the function with batches of records:

DynamoDB Stream Processing


// Lambda function for processing DynamoDB streams
exports.handler = async (event) => {
  console.log('Event:', JSON.stringify(event, null, 2));
  
  // Process each record in the batch
  for (const record of event.Records) {
    // Get the DynamoDB stream event type
    const eventType = record.eventName; // INSERT, MODIFY, REMOVE
    
    // Get the new and old images (if available)
    const newImage = record.dynamodb.NewImage 
      ? AWS.DynamoDB.Converter.unmarshall(record.dynamodb.NewImage) 
      : null;
      
    const oldImage = record.dynamodb.OldImage 
      ? AWS.DynamoDB.Converter.unmarshall(record.dynamodb.OldImage) 
      : null;
    
    console.log(`Processing ${eventType} event:`, { oldImage, newImage });
    
    switch (eventType) {
      case 'INSERT':
        await handleInsert(newImage);
        break;
      case 'MODIFY':
        await handleUpdate(oldImage, newImage);
        break;
      case 'REMOVE':
        await handleDelete(oldImage);
        break;
    }
  }
  
  console.log(`Successfully processed ${event.Records.length} records`);
  return { status: 'success', count: event.Records.length };
};

async function handleInsert(item) {
  // Handle item creation
  console.log(`New item created: ${item.id}`);
  // Example: update search index, send notification, etc.
}

async function handleUpdate(oldItem, newItem) {
  // Handle item update
  console.log(`Item updated: ${newItem.id}`);
  // Example: track changes, update aggregates, etc.
}

async function handleDelete(item) {
  // Handle item deletion
  console.log(`Item deleted: ${item.id}`);
  // Example: clean up related resources, archive data, etc.
}
        

Lambda Configuration and Security

Environment Variables

Environment variables allow you to pass configuration to your Lambda function:


// Using environment variables
exports.handler = async (event) => {
  // Access environment variables
  const tableName = process.env.TABLE_NAME;
  const region = process.env.AWS_REGION;
  const stage = process.env.STAGE || 'dev';
  const apiKey = process.env.API_KEY;
  
  console.log(`Running in ${stage} stage, using table: ${tableName}`);
  
  // Use the environment variables
  const dynamoDB = new AWS.DynamoDB.DocumentClient({ region });
  
  const params = {
    TableName: tableName,
    KeyConditionExpression: 'id = :id',
    ExpressionAttributeValues: {
      ':id': event.id
    }
  };
  
  try {
    const result = await dynamoDB.query(params).promise();
    
    return {
      statusCode: 200,
      body: JSON.stringify(result.Items)
    };
  } catch (error) {
    console.error('Error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ message: 'Internal server error' })
    };
  }
};
      

Environment Variables Best Practices

  • Use for configuration: Connection strings, feature flags, API endpoints
  • Environment separation: Development, testing, production settings
  • Don't store secrets: Use AWS Secrets Manager or Parameter Store for sensitive values
  • Define defaults: Always provide fallback values for optional settings
  • Keep size small: Environment variables contribute to cold start time

IAM Roles and Permissions

Lambda functions use IAM roles to securely access AWS services and resources:


// Example IAM policy for a Lambda function that accesses DynamoDB and S3
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Query",
        "dynamodb:Scan"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/MyTable"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}
      

IAM Best Practices for Lambda

  • Principle of least privilege: Grant only the permissions needed
  • Use resource-level permissions: Restrict access to specific resources
  • Separate roles per function: Don't reuse roles across functions
  • Use conditions: Further restrict access based on context
  • Audit regularly: Review and remove unused permissions

VPC Configuration

Lambda functions can be configured to access resources in a private VPC:

graph TB A[Lambda Function] --> B[ENI in VPC] B --> C[Private Subnet] C --> D[RDS Database] C --> E[ElastiCache] C --> F[Internal Services] G[Public Resources] --> A subgraph Virtual Private Cloud B C D E F end

VPC Considerations

  • Cold start impact: VPC-connected functions have longer cold starts
  • Internet access: Requires NAT Gateway for outbound internet access
  • Security groups: Control inbound/outbound traffic
  • ENI limits: Be aware of Elastic Network Interface limits in your account
  • Only use when needed: Only configure VPC access if you need to access private resources

Lambda Deployment Strategies

There are several ways to deploy Lambda functions:

Deployment Packages

A deployment package is a ZIP or JAR file containing your function code and dependencies:


# Create a deployment package for Node.js
mkdir my-function
cd my-function

# Initialize npm project
npm init -y

# Install dependencies
npm install aws-sdk uuid axios

# Create function code
# ... (add your function code to index.js)

# Create deployment package
zip -r function.zip .

# Deploy the function
aws lambda update-function-code \
  --function-name my-function \
  --zip-file fileb://function.zip
      

Managing Dependencies

Node.js Layers Example:


# Create a layer for common dependencies
mkdir nodejs
cd nodejs
npm init -y
npm install aws-sdk axios lodash moment

# Create layer ZIP
cd ..
zip -r layer.zip nodejs

# Create Lambda layer
aws lambda publish-layer-version \
  --layer-name common-dependencies \
  --description "Common Node.js dependencies" \
  --zip-file fileb://layer.zip \
  --compatible-runtimes nodejs14.x

# Note the LayerVersionArn from the response

# Update function to use the layer
aws lambda update-function-configuration \
  --function-name my-function \
  --layers arn:aws:lambda:us-east-1:123456789012:layer:common-dependencies:1
        

AWS SAM (Serverless Application Model)

AWS SAM is an open-source framework for building serverless applications:


# Example SAM template
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./src/
      Handler: app.handler
      Runtime: nodejs14.x
      Architectures:
        - x86_64
      MemorySize: 128
      Timeout: 10
      Environment:
        Variables:
          TABLE_NAME: !Ref DynamoDBTable
          STAGE: !Ref Stage
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref DynamoDBTable
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

  DynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

Parameters:
  Stage:
    Type: String
    Default: dev
    AllowedValues:
      - dev
      - test
      - prod
    Description: Stage for the application

Outputs:
  HelloWorldFunction:
    Description: "Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldApi:
    Description: "API Gateway endpoint URL"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
      

Deploy with SAM CLI


# Build the application
sam build

# Deploy the application
sam deploy --guided

# Follow the interactive prompts to configure deployment options
        

Serverless Framework

The Serverless Framework is a popular toolkit for building and deploying serverless applications:


# Example serverless.yml
service: my-service

provider:
  name: aws
  runtime: nodejs14.x
  stage: ${opt:stage, 'dev'}
  region: ${opt:region, 'us-east-1'}
  environment:
    TABLE_NAME: ${self:service}-${self:provider.stage}-table
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: !GetAtt DynamoDBTable.Arn

functions:
  api:
    handler: src/handler.api
    events:
      - http:
          path: /users
          method: get
      - http:
          path: /users/{id}
          method: get
      - http:
          path: /users
          method: post
      - http:
          path: /users/{id}
          method: put
      - http:
          path: /users/{id}
          method: delete

resources:
  Resources:
    DynamoDBTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.TABLE_NAME}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST
      

Deploy with Serverless Framework


# Install Serverless Framework
npm install -g serverless

# Create a new service
serverless create --template aws-nodejs --path my-service

# Install dependencies
cd my-service
npm install

# Deploy the service
serverless deploy
        

Best Practices for Lambda Functions

Follow these best practices to build efficient, scalable, and maintainable Lambda functions:

Performance Optimization

Connection Reuse Example


const AWS = require('aws-sdk');
const mysql = require('mysql2/promise');

// Configure AWS SDK outside the handler
AWS.config.update({ region: 'us-east-1' });

// Initialize clients outside the handler
const s3 = new AWS.S3();
const dynamoDB = new AWS.DynamoDB.DocumentClient();

// Database connection pool - initialized once per container
let connectionPool;

async function getConnectionPool() {
  if (connectionPool) {
    return connectionPool;
  }
  
  // Initialize connection pool
  connectionPool = await mysql.createPool({
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    waitForConnections: true,
    connectionLimit: 10,
    queueLimit: 0
  });
  
  return connectionPool;
}

exports.handler = async (event) => {
  // Get database connection from the pool
  const pool = await getConnectionPool();
  
  try {
    // Use the connection pool
    const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [event.userId]);
    
    // Use the S3 client
    const imageData = await s3.getObject({
      Bucket: 'my-bucket',
      Key: `users/${event.userId}/profile.jpg`
    }).promise();
    
    // Use the DynamoDB client
    const userPrefs = await dynamoDB.get({
      TableName: 'UserPreferences',
      Key: { userId: event.userId }
    }).promise();
    
    return {
      statusCode: 200,
      body: JSON.stringify({
        user: rows[0],
        preferences: userPrefs.Item,
        imageUrl: `https://my-bucket.s3.amazonaws.com/users/${event.userId}/profile.jpg`
      })
    };
  } catch (error) {
    console.error('Error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ message: 'Internal server error' })
    };
  }
};
        

Error Handling

Idempotent Operation Example


// Example of an idempotent handler for order processing
exports.handler = async (event) => {
  for (const record of event.Records) {
    try {
      // Parse the SQS message
      const message = JSON.parse(record.body);
      const orderId = message.orderId;
      
      // Check if this order has already been processed
      const orderStatus = await checkOrderStatus(orderId);
      
      if (orderStatus === 'PROCESSED' || orderStatus === 'PROCESSING') {
        console.log(`Order ${orderId} already processed or processing, skipping`);
        continue;
      }
      
      // Mark order as processing
      await updateOrderStatus(orderId, 'PROCESSING');
      
      // Process the order
      await processOrder(orderId, message.orderDetails);
      
      // Mark order as processed
      await updateOrderStatus(orderId, 'PROCESSED');
      
      console.log(`Successfully processed order ${orderId}`);
    } catch (error) {
      console.error('Error processing record:', error);
      // Let the Lambda retry the message
      throw error;
    }
  }
  
  return { status: 'success' };
};

async function checkOrderStatus(orderId) {
  // Implementation to check order status in database
}

async function updateOrderStatus(orderId, status) {
  // Implementation to update order status in database
}

async function processOrder(orderId, orderDetails) {
  // Implementation to process the order
}
        

Monitoring and Logging

Structured Logging Example


// Helper function for structured logging
function log(level, message, data = {}) {
  const logEntry = {
    level,
    message,
    timestamp: new Date().toISOString(),
    ...data
  };
  
  console.log(JSON.stringify(logEntry));
}

exports.handler = async (event, context) => {
  // Generate a correlation ID or use one from the event
  const correlationId = event.headers?.['x-correlation-id'] || context.awsRequestId;
  
  // Log with context
  log('info', 'Function invoked', {
    correlationId,
    functionName: context.functionName,
    functionVersion: context.functionVersion,
    eventSource: event.requestContext?.eventSource || 'direct'
  });
  
  try {
    // Process business logic
    const result = await processRequest(event, correlationId);
    
    // Log success
    log('info', 'Request processed successfully', {
      correlationId,
      executionTime: Date.now() - context.invokeStartTime,
      responseSize: JSON.stringify(result).length
    });
    
    return {
      statusCode: 200,
      body: JSON.stringify(result)
    };
  } catch (error) {
    // Log error with details
    log('error', 'Error processing request', {
      correlationId,
      errorMessage: error.message,
      errorName: error.name,
      errorStack: error.stack,
      errorCode: error.code
    });
    
    return {
      statusCode: error.statusCode || 500,
      body: JSON.stringify({
        message: error.message,
        reference: correlationId
      })
    };
  }
};

async function processRequest(event, correlationId) {
  // Business logic implementation
  log('debug', 'Processing request', {
    correlationId,
    step: 'started'
  });
  
  // Processing steps...
  
  log('debug', 'Request processed', {
    correlationId,
    step: 'completed'
  });
  
  return { result: 'success' };
}
        

Practical Exercise

Building a Serverless API with AWS Lambda

In this exercise, you'll create a serverless API for a simple note-taking application with the following features:

Requirements:

  1. Create Lambda functions for each API operation
  2. Set up an API Gateway to expose the functions
  3. Use DynamoDB for data storage
  4. Implement proper error handling and validation
  5. Apply best practices for performance and security

Sample Code Structure


// Project structure
notes-api/
├── serverless.yml         # Infrastructure definition
├── package.json           # Dependencies
├── src/
│   ├── handlers/
│   │   ├── createNote.js  # Create note handler
│   │   ├── getNote.js     # Get note handler
│   │   ├── listNotes.js   # List notes handler
│   │   ├── updateNote.js  # Update note handler
│   │   └── deleteNote.js  # Delete note handler
│   ├── lib/
│   │   ├── dynamodb.js    # DynamoDB client
│   │   ├── response.js    # Response formatting
│   │   └── validator.js   # Input validation
│   └── models/
│       └── Note.js        # Note data model
└── tests/
    └── ...                # Unit tests
        

Example createNote.js:


const { v4: uuidv4 } = require('uuid');
const dynamodb = require('../lib/dynamodb');
const { formatResponse, formatError } = require('../lib/response');
const { validateNote } = require('../lib/validator');

const TABLE_NAME = process.env.NOTES_TABLE;

exports.handler = async (event) => {
  try {
    // Parse request body
    const body = JSON.parse(event.body);
    
    // Validate input
    const { valid, errors } = validateNote(body);
    if (!valid) {
      return formatError(400, 'Invalid input', errors);
    }
    
    // Create note item
    const note = {
      id: uuidv4(),
      userId: body.userId,
      title: body.title,
      content: body.content,
      category: body.category || 'general',
      important: body.important || false,
      completed: false,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };
    
    // Save to DynamoDB
    await dynamodb.put({
      TableName: TABLE_NAME,
      Item: note
    }).promise();
    
    return formatResponse(201, note);
  } catch (error) {
    console.error('Error creating note:', error);
    return formatError(500, 'Could not create note');
  }
};
        

Conclusion and Key Takeaways

In the next lecture, we'll explore building complete serverless APIs with Lambda, API Gateway, and other AWS services.

Additional Resources