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
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:
- Log in to the AWS Management Console
- Navigate to the Lambda service
- Click "Create function"
- Choose "Author from scratch", provide a name, and select a runtime
- Create or select an execution role with appropriate permissions
- Configure basic settings (memory, timeout, etc.)
- Write or upload your function code
- Configure triggers and destinations
- Click "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:
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: HTTP(S) requests
- Application Load Balancer: HTTP(S) requests
- AWS SDK: Direct invocation
- Amazon Cognito: Authentication events
- Amazon Lex: Chatbot interactions
API Gateway Integration
// 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:
- Amazon S3: Object created/deleted events
- Amazon SNS: Notification events
- Amazon EventBridge: Scheduled or custom events
- AWS SDK: Asynchronous invocation
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 Streams: Table changes
- Kinesis Data Streams: Data records
- Amazon SQS: Message processing
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:
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
- Minimize cold starts: Keep your deployment package small, use appropriate languages
- Reuse connections: Initialize clients outside the handler
- Cache external configuration: Load and cache configuration during initialization
- Optimize memory settings: Test different memory settings to find the optimal balance of cost and performance
- Use Provisioned Concurrency: For latency-sensitive applications
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
- Catch and handle exceptions: Prevent unexpected termination
- Use proper status codes: Return appropriate HTTP status codes for API responses
- Include meaningful error messages: Provide enough information for debugging
- Handle retries: Implement idempotent operations for asynchronous functions
- Use DLQs: Configure Dead Letter Queues for asynchronous functions
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: Use JSON format for easier querying
- Include correlation IDs: Track requests across distributed systems
- Set up CloudWatch alarms: Monitor for errors, throttling, and duration
- Use X-Ray tracing: Trace requests through distributed components
- Implement custom metrics: Track business-specific metrics in CloudWatch
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:
- Create, read, update, and delete notes
- List all notes or filter by category
- Mark notes as important or completed
Requirements:
- Create Lambda functions for each API operation
- Set up an API Gateway to expose the functions
- Use DynamoDB for data storage
- Implement proper error handling and validation
- 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
- AWS Lambda is a serverless compute service that runs your code in response to events
- Lambda functions have a handler that receives event data and returns a response
- The execution environment includes initialization and invocation phases, with state persistence between invocations
- Memory allocation affects both CPU performance and cost
- Various event sources can trigger Lambda functions, including API Gateway, S3, DynamoDB, and more
- IAM roles provide secure access to AWS resources
- Deployment options include direct uploads, AWS SAM, and the Serverless Framework
- Best practices include optimizing for cold starts, reusing connections, proper error handling, and effective monitoring
In the next lecture, we'll explore building complete serverless APIs with Lambda, API Gateway, and other AWS services.