Claude Code × AWS Lambda Complete Guide | From Function Generation to Deployment Automation
Build safer AWS Lambda apps with Claude Code: Node.js 24, SAM, IAM, API Gateway, S3, logs, and pitfalls.
Have you ever experienced this with AWS Lambda development? Type definitions for handlers are tedious, you have to look up IAM policies every time, you forgot how to write SAM templates… Claude Code solves all of these at once.
From Lambda function implementation to IAM policy generation, local testing, and production deployment — we’ll walk through every step of blazing-fast AWS Lambda development with Claude Code using real code examples.
Why Claude Code × AWS Lambda?
The “tedious parts” of Lambda development are almost entirely boilerplate.
- Handler function type definitions (APIGatewayProxyHandler, S3Handler …)
- Error handling and response formats
- Least-privilege IAM policy design
- SAM / CloudFormation template writing
- Local test environment configuration
Claude Code generates all of these from a single sentence like “I want a Lambda that does X”. Combined with the AWS CLI and SAM CLI, you can run everything from code generation to deployment in one go.
Environment Setup
# Verify required tools
aws --version # AWS CLI v2
sam --version # SAM CLI 1.100+
node --version # Node.js 24+
# Configure AWS credentials (if not already set)
aws configure
# → Access Key ID, Secret Access Key, Region (us-east-1), Output (json)
<!-- Add to CLAUDE.md -->
## AWS Configuration
- Default region: us-east-1
- Lambda runtime: nodejs24.x
- Deployment tool: AWS SAM
- Source directory: src/functions/
- SAM template: template.yaml
- Stack name: my-app-{dev|staging|prod}
## IAM Policy Guidelines
- Strictly follow the principle of least privilege
- Do not use wildcards (*)
- Specify resource ARNs concretely
Example 1: API Gateway + Lambda (REST API)
The most fundamental pattern — a Lambda that receives HTTP requests and processes them.
Instructions to Claude Code
claude -p "
When a GET request comes to /users/{userId},
retrieve user information from the DynamoDB Users table and return it.
Implement the Lambda function in TypeScript.
- Runtime: Node.js 24
- Table name: UsersTable (received via environment variable)
- Error handling: return appropriate 404/500 responses
- Type definitions: use @types/aws-lambda
- Output to: src/functions/getUser/index.ts
"
Generated Implementation
// src/functions/getUser/index.ts
import {
APIGatewayProxyHandler,
APIGatewayProxyResult,
} from "aws-lambda";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({ region: process.env.AWS_REGION });
const ddb = DynamoDBDocumentClient.from(client);
const response = (statusCode: number, body: unknown): APIGatewayProxyResult => ({
statusCode,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(body),
});
export const handler: APIGatewayProxyHandler = async (event) => {
const userId = event.pathParameters?.userId;
if (!userId) {
return response(400, { error: "userId is required" });
}
try {
const result = await ddb.send(
new GetCommand({
TableName: process.env.USERS_TABLE!,
Key: { userId },
})
);
if (!result.Item) {
return response(404, { error: `User '${userId}' not found` });
}
return response(200, result.Item);
} catch (err) {
console.error("DynamoDB error:", err);
return response(500, { error: "Internal server error" });
}
};
SAM Template Also Auto-Generated
# template.yaml (excerpt)
GetUserFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/functions/getUser/index.handler
Runtime: nodejs24.x
Timeout: 10
Environment:
Variables:
USERS_TABLE: !Ref UsersTable
Policies:
- DynamoDBReadPolicy:
TableName: !Ref UsersTable
Events:
GetUserApi:
Type: Api
Properties:
Path: /users/{userId}
Method: GET
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub "${AWS::StackName}-users"
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
Example 2: S3 Event Trigger Lambda
A Lambda triggered by file uploads to run processing.
// src/functions/generateThumbnail/index.ts
import { S3Handler } from "aws-lambda";
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import sharp from "sharp";
const s3 = new S3Client({ region: process.env.AWS_REGION });
export const handler: S3Handler = async (event) => {
const record = event.Records[0];
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));
// Skip thumbnails/ prefix (prevent infinite loop)
if (key.startsWith("thumbnails/")) return;
const { Body } = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
const buffer = Buffer.from(await Body!.transformToByteArray());
const thumbnail = await sharp(buffer)
.resize(200, 200, { fit: "cover" })
.webp({ quality: 85 })
.toBuffer();
const thumbnailKey = `thumbnails/${key.replace(/\.[^.]+$/, ".webp")}`;
await s3.send(
new PutObjectCommand({
Bucket: bucket,
Key: thumbnailKey,
Body: thumbnail,
ContentType: "image/webp",
})
);
console.log(`Thumbnail created: s3://${bucket}/${thumbnailKey}`);
};
Example 3: Scheduled Lambda (EventBridge)
# EventBridge configuration in template.yaml
SendReminderFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/functions/sendReminder/index.handler
Runtime: nodejs24.x
Timeout: 300
Environment:
Variables:
DATABASE_URL: !Sub "{{resolve:secretsmanager:${AWS::StackName}/database-url}}"
RESEND_API_KEY: !Sub "{{resolve:secretsmanager:${AWS::StackName}/resend-api-key}}"
Events:
DailyReminder:
Type: Schedule
Properties:
Schedule: cron(0 0 * * ? *) # UTC 0:00 = EST 7:00 PM / PST 4:00 PM
Let Claude Code Design IAM Policies
claude -p "
Generate a minimum-privilege IAM policy in JSON for a Lambda that needs:
- GetObject from S3 bucket my-uploads
- PutItem/UpdateItem on DynamoDB table ProcessingJobs
- SendMessage to SQS ProcessingQueue
- Write to CloudWatch Logs
Infer specific ARNs from the resource names provided.
"
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-uploads/*"
},
{
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:UpdateItem"],
"Resource": "arn:aws:dynamodb:us-east-1:*:table/ProcessingJobs"
},
{
"Effect": "Allow",
"Action": ["sqs:SendMessage"],
"Resource": "arn:aws:sqs:us-east-1:*:ProcessingQueue"
},
{
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:*:*:log-group:/aws/lambda/*"
}
]
}
Local Testing and Production Deployment
# Build
sam build
# Start API locally
sam local start-api --port 3001
# One-off test invocation
sam local invoke GetUserFunction --event events/get-user.json
# Run everything with Claude Code
claude -p "
Run sam build,
verify results with sam local invoke GetUserFunction --event events/test-get-user.json.
If everything looks good, run sam deploy --config-env dev.
"
Top 5 Pitfalls
1. Initialization that ignores cold starts
// ❌ Creating a client inside the handler every time
export const handler = async () => {
const ddb = new DynamoDBClient({}); // ← instantiated on every invocation
};
// ✅ Initialize once at module scope
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const handler = async () => { /* reuse ddb */ };
2. Leaving the timeout at the default (3 seconds) For DynamoDB + external APIs, set at least 10-30 seconds. Always configure based on actual processing requirements.
3. Writing secrets directly in environment variables
# ❌ Hard-coded in the template
Environment:
Variables:
DB_PASSWORD: "my-secret"
# ✅ Via Secrets Manager
Environment:
Variables:
DB_PASSWORD: !Sub "{{resolve:secretsmanager:myapp/db-password}}"
4. Cold start delays with VPC Lambda Placing a Lambda inside a VPC for RDS connectivity adds several seconds to cold starts. Address with Provisioned Concurrency or RDS Proxy.
5. Oversized deployment packages
Packaging node_modules together can hit the 250 MB limit. Move shared libraries to a Lambda Layer.
Summary
| Task | Claude Code’s Contribution |
|---|---|
| Handler implementation | Generate type definitions, error handling, and logic in one shot |
| SAM template | Auto-output events, IAM, and environment variables |
| IAM policy | Accurately generate least-privilege designs |
| Local testing | Automate sam invoke execution and result evaluation |
| Deployment | Execute build + deploy as a single workflow |
Claude Code takes over the most time-consuming parts of Lambda development — type definitions and template writing. Focusing on business logic can push implementation speed up 3-5x.
When Lambda Fits and When It Does Not
Lambda fits short, event-shaped work with clear input and output: order lookup APIs, image processing after S3 upload, daily EventBridge reports, and Stripe or GitHub webhooks. It is a weak fit for long-running workers, persistent low-latency connections, large warm models, and stateful WebSocket coordination. Ask Claude Code to explain both why Lambda fits and why it might not fit before it generates code.
Plain-Language Terms
| Term | Plain meaning |
|---|---|
| handler | The function Lambda calls. API Gateway passes an HTTP event; S3 passes an object event. |
| event | The JSON payload given to Lambda. API Gateway, S3, and EventBridge shapes are different. |
| least privilege | Grant only the exact actions and resources the function needs. |
| cold start | The slower first run when Lambda creates a new execution environment. |
| idempotency | The same event can run twice without duplicate payment, email, or database writes. |
Copy-Paste Unit Test for Node.js 24
// test/getUser.test.ts
import { describe, expect, it, vi } from "vitest";
import type { APIGatewayProxyEvent } from "aws-lambda";
const mockSend = vi.fn();
vi.mock("@aws-sdk/lib-dynamodb", async () => {
const actual = await vi.importActual<typeof import("@aws-sdk/lib-dynamodb")>("@aws-sdk/lib-dynamodb");
return { ...actual, DynamoDBDocumentClient: { from: () => ({ send: mockSend }) } };
});
const { handler } = await import("../src/functions/getUser/index");
function event(userId?: string): APIGatewayProxyEvent {
return {
pathParameters: userId ? { userId } : null,
httpMethod: "GET",
path: userId ? "/users/" + userId : "/users",
headers: {},
multiValueHeaders: {},
queryStringParameters: null,
multiValueQueryStringParameters: null,
body: null,
isBase64Encoded: false,
requestContext: {} as APIGatewayProxyEvent["requestContext"],
resource: "/users/{userId}",
stageVariables: null,
};
}
describe("getUser Lambda", () => {
it("returns 200 when the user exists", async () => {
process.env.USERS_TABLE = "UsersTable";
mockSend.mockResolvedValueOnce({ Item: { userId: "u-1", name: "Masa" } });
const res = await handler(event("u-1"), {} as never, vi.fn());
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.body).userId).toBe("u-1");
});
it("returns 400 when path parameter is missing", async () => {
const res = await handler(event(), {} as never, vi.fn());
expect(res.statusCode).toBe(400);
});
});
Keep API Gateway, S3, and EventBridge Event Shapes Separate
API Gateway requires an HTTP response with statusCode and body. S3 handlers read bucket and key from Records. EventBridge handlers usually inspect source, detail-type, and detail. If you mix these types, local tests can pass while production events fail. Name the trigger explicitly in the Claude Code prompt: HTTP API v2, S3 ObjectCreated, or EventBridge schedule.
Environment Variables, Secrets Manager, and Least-Privilege IAM
Use environment variables for configuration such as USERS_TABLE and APP_STAGE. Store API keys, database passwords, and webhook signing secrets in Secrets Manager, then pass only the Secret ARN to Lambda. IAM should use specific actions such as dynamodb:GetItem and s3:GetObject with concrete resource ARNs. If Claude Code emits Resource: "*" or s3:*, run another least-privilege review.
CloudWatch Logs, Retries, and Idempotency
CloudWatch Logs is the first place to investigate Lambda behavior. If logs are missing, check region, function name, execution role permissions for logs:CreateLogStream and logs:PutLogEvents, and the deployed stack name. S3, EventBridge, and asynchronous invokes can retry, so use event IDs or business IDs as idempotency keys. Duplicate email, duplicate billing, and recursive writes to the same S3 key prefix are common Lambda failures.
Claude Code Security Review Prompt
Review this AWS Lambda change before production.
Check handler shape, API Gateway/S3/EventBridge event type, IAM least privilege, Secrets Manager usage, CloudWatch Logs, retries, idempotency, cold starts, timeout, memory, and cost risk.
Return blockers first, then recommended fixes, missing tests, and human approvals required before deploy.
Monetization, Training, and Consultation CTA
ClaudeCodeLab offers training and consultation for Lambda, IAM, SAM, log audits, and Claude Code review gates. Solo builders can start with the free PDF and Gumroad materials; teams shipping production workloads should use training and consultation to design permissions, deploy approvals, and revenue paths together.
Hands-On Result
I split the examples into a handler and tests that can be checked locally with npm test. I did not run sam deploy against a real AWS account because account ID, region, Secret ARN, and DynamoDB table names are environment-specific. In production, keep a verification note for sam validate --lint, sam build, sam local invoke, CloudWatch Logs, and IAM diff review.
Additional Official Links
- Lambda runtimes
- Building Lambda functions with Node.js
- Working with Lambda environment variables
- Security best practices in IAM
- Sending Lambda function logs to CloudWatch Logs
- Invoking Lambda with API Gateway
- Using an S3 trigger to invoke Lambda
- Retries and failures in Lambda applications
Internal Link Checklist
- AWS IAM guide
- AWS API Gateway guide
- AWS S3 guide
- AWS CloudWatch guide
- serverless functions guide
- security best practices
Related Articles
- Claude Code × AWS S3 Operation Automation
- Claude Code × AWS DynamoDB Schema Design
- Claude Code Security Best Practices Complete Guide
References
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.