Claude Code AWS Lambda Guide for Teams: Node.js, IAM, API Gateway
Build a Node.js AWS Lambda with Claude Code, local tests, least-privilege IAM, environment variables, API Gateway, and logs.
Claude Code is useful for AWS Lambda because one request can produce the handler, event fixture, IAM policy draft, deployment commands, and review checklist. That speed is valuable, but AWS permissions and costs are real production decisions. Let Claude Code draft and check. Do not let it blindly grant IAM permissions, expose a public API, or create billable resources without human review.
This guide walks through a practical team workflow: create a Node.js Lambda, test it locally, keep IAM close to least privilege, configure environment variables, connect API Gateway, read CloudWatch Logs, update the zip deployment package, and run a safe review loop. Lambda is a small runtime that starts when an event arrives. IAM is the permission model that decides what the function can do. API Gateway is the HTTP entry point that forwards requests to Lambda.
Keep the official docs nearby: AWS Lambda getting started, Lambda with Node.js, Lambda environment variables, CloudWatch Logs monitoring, and Claude Code common workflows. The examples below use the Node.js 24 Lambda runtime as of June 1, 2026.
When This Pattern Fits
Lambda works well when you need a small, reliable action without running a server all day. Claude Code helps most when the task has repeatable structure and a reviewer still owns the final decision.
| Use case | Why Lambda fits | Let Claude Code draft | Human review must cover |
|---|---|---|---|
| Webhook receiver | Payment, form, and SaaS events are short-lived | Signature checks, event fixtures, failure responses | Secrets, retries, duplicate events |
| Internal JSON API | Small endpoints do not need a persistent app server | Handler, API Gateway commands, log format | Auth, CORS, exposure, cost |
| Batch entry point | CSV imports, image jobs, and notifications can start small | Input validation, structured logs, error classes | Timeout, retry strategy, safe deletion |
For a first proof of concept, AWS CLI plus a zip package is fast. For team operations, move the same logic into SAM, CDK, or another reviewed IaC flow.
| Approach | Best for | Watch out for |
|---|---|---|
| AWS console | One-time behavior check | Manual steps disappear from review history |
| AWS CLI + zip | Small reproducible proof of concept | IAM and env vars need careful review |
| SAM / CDK | Long-lived team workflow | IaC and deployment approvals become mandatory |
flowchart LR
A[Draft with Claude Code] --> B[Run local Node.js test]
B --> C[Human reviews IAM and environment]
C --> D[Deploy zip package to dev]
D --> E[Test through API Gateway]
E --> F[Read CloudWatch Logs]
F --> A
1. Create the Node.js Lambda Handler
Start with a dependency-free index.mjs. It accepts both HTTP API v2 style events and older REST API style events, then returns the shape API Gateway expects.
// index.mjs
import crypto from "node:crypto";
const allowedStages = new Set(["dev", "staging", "prod"]);
function readConfig() {
const stage = process.env.APP_STAGE ?? "dev";
if (!allowedStages.has(stage)) {
throw new Error(`Invalid APP_STAGE: ${stage}`);
}
return {
stage,
tableName: process.env.TABLE_NAME ?? "local-orders",
logLevel: process.env.LOG_LEVEL ?? "info",
};
}
function log(level, message, details = {}) {
console.log(
JSON.stringify({
level,
message,
service: "orders-api",
...details,
})
);
}
function json(statusCode, body) {
return {
statusCode,
headers: {
"content-type": "application/json",
},
body: JSON.stringify(body),
};
}
function parseBody(event) {
if (!event.body) return {};
const raw = event.isBase64Encoded
? Buffer.from(event.body, "base64").toString("utf8")
: event.body;
return JSON.parse(raw);
}
export async function handler(event = {}, context = {}) {
const config = readConfig();
const method = event.requestContext?.http?.method ?? event.httpMethod ?? "GET";
const path = event.rawPath ?? event.path ?? "/";
const requestId = context.awsRequestId ?? "local";
log("info", "request.start", {
requestId,
method,
path,
stage: config.stage,
});
try {
if (method === "GET" && path === "/health") {
return json(200, { ok: true, stage: config.stage });
}
if (method === "POST" && path === "/orders") {
const payload = parseBody(event);
const orderId = payload.orderId ?? crypto.randomUUID();
log("info", "order.accepted", {
requestId,
orderId,
tableName: config.tableName,
});
return json(202, {
orderId,
status: "accepted",
storedIn: config.tableName,
});
}
return json(404, { error: "not_found", method, path });
} catch (error) {
log("error", "request.failed", {
requestId,
errorName: error.name,
errorMessage: error.message,
});
return json(500, { error: "internal_error", requestId });
}
}
This first version does not write to DynamoDB. That is intentional. For beginners, the first milestone is receiving an event, parsing JSON, returning a valid response, and producing logs that can be searched later.
2. Test Locally With an Event Fixture
An event fixture is a saved sample input. Ask Claude Code to generate one, then keep it in the repo so reviewers can reproduce failures.
{
"version": "2.0",
"routeKey": "POST /orders",
"rawPath": "/orders",
"requestContext": {
"http": {
"method": "POST",
"path": "/orders"
}
},
"headers": {
"content-type": "application/json"
},
"body": "{\"orderId\":\"demo-1001\",\"amount\":3200,\"currency\":\"JPY\"}",
"isBase64Encoded": false
}
Save it as events/create-order.json, then add a tiny local runner.
// local-test.mjs
import { readFile } from "node:fs/promises";
import { handler } from "./index.mjs";
process.env.APP_STAGE = "dev";
process.env.TABLE_NAME = "orders-dev";
process.env.LOG_LEVEL = "debug";
const eventPath = process.argv[2] ?? "events/create-order.json";
const event = JSON.parse(await readFile(eventPath, "utf8"));
const result = await handler(event, { awsRequestId: "local-001" });
console.log(JSON.stringify(result, null, 2));
Run it before touching AWS:
node local-test.mjs events/create-order.json
If the local result does not return statusCode: 202, deploying will only make the problem slower to debug. Local fixtures also make Claude Code more useful because it can reason from a concrete failing input instead of guessing.
3. Keep IAM Close to Least Privilege
Do not attach AdministratorAccess to a Lambda execution role. Start with logs, then add data access only when the handler actually needs it.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CreateOwnLogGroupIfMissing",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:ap-northeast-1:123456789012:*"
},
{
"Sid": "WriteOwnLambdaLogs",
"Effect": "Allow",
"Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/claude-orders-dev:*"
},
{
"Sid": "ReadWriteOnlyOrdersTable",
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:Query"],
"Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/orders-dev"
}
]
}
Treat this as a draft, not a production answer. Replace the account, region, function name, and table name. Delete the DynamoDB block until you actually call DynamoDB. For a deeper permission review, read the internal AWS IAM guide.
4. Deploy a Zip Package With AWS CLI
The next commands create real AWS resources and can generate cost. Use a dev account, confirm the region, and know how to delete everything before you run them.
export AWS_REGION=ap-northeast-1
export FUNCTION_NAME=claude-orders-dev
export ROLE_NAME=claude-orders-dev-lambda-role
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
zip function.zip index.mjs
Create trust-policy.json so Lambda can assume the role.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Save the previous IAM draft as lambda-policy.json, then create the role and function.
aws iam create-role \
--role-name "$ROLE_NAME" \
--assume-role-policy-document file://trust-policy.json
aws iam put-role-policy \
--role-name "$ROLE_NAME" \
--policy-name claude-orders-dev-inline \
--policy-document file://lambda-policy.json
ROLE_ARN=$(aws iam get-role \
--role-name "$ROLE_NAME" \
--query "Role.Arn" \
--output text)
aws lambda create-function \
--function-name "$FUNCTION_NAME" \
--runtime nodejs24.x \
--handler index.handler \
--zip-file fileb://function.zip \
--role "$ROLE_ARN" \
--architectures arm64 \
--timeout 10 \
--memory-size 128 \
--environment "Variables={APP_STAGE=dev,TABLE_NAME=orders-dev,LOG_LEVEL=info}" \
--region "$AWS_REGION"
Update code without recreating the function:
zip function.zip index.mjs
aws lambda update-function-code \
--function-name "$FUNCTION_NAME" \
--zip-file fileb://function.zip \
--region "$AWS_REGION"
Be careful with environment variable updates. With update-function-configuration, the Variables map is replaced as a whole. Fetch the current configuration before changing it, and keep secrets in Secrets Manager or Parameter Store instead of plain environment variables.
aws lambda update-function-configuration \
--function-name "$FUNCTION_NAME" \
--environment "Variables={APP_STAGE=dev,TABLE_NAME=orders-dev,LOG_LEVEL=debug}" \
--region "$AWS_REGION"
5. Verify API Gateway and CloudWatch Logs
Invoke Lambda directly first.
aws lambda invoke \
--function-name "$FUNCTION_NAME" \
--payload fileb://events/create-order.json \
--region "$AWS_REGION" \
response.json
cat response.json
Then create an HTTP API in API Gateway. This exposes a public endpoint, so review auth, CORS, rate limits, and deletion plans before running it.
API_ID=$(aws apigatewayv2 create-api \
--name claude-orders-dev-api \
--protocol-type HTTP \
--target "arn:aws:lambda:${AWS_REGION}:${ACCOUNT_ID}:function:${FUNCTION_NAME}" \
--query "ApiId" \
--output text)
aws lambda add-permission \
--function-name "$FUNCTION_NAME" \
--statement-id AllowHttpApiInvoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:${AWS_REGION}:${ACCOUNT_ID}:${API_ID}/*/*" \
--region "$AWS_REGION"
API_ENDPOINT=$(aws apigatewayv2 get-api \
--api-id "$API_ID" \
--query "ApiEndpoint" \
--output text)
curl -s "$API_ENDPOINT/health"
curl -s -X POST "$API_ENDPOINT/orders" \
-H "content-type: application/json" \
-d '{"amount":3200,"currency":"JPY"}'
Read the logs immediately after testing.
aws logs tail "/aws/lambda/${FUNCTION_NAME}" \
--follow \
--region "$AWS_REGION"
If no logs appear, check the region, function name, and logs:CreateLogStream plus logs:PutLogEvents permissions. If API Gateway returns 500, confirm the Lambda response contains statusCode, headers, and body. For more depth, continue with the internal AWS CloudWatch guide and AWS API Gateway guide.
Claude Prompt for a Lambda Review
Use Claude Code to explain risk before it edits or deploys anything.
You are reviewing a Node.js AWS Lambda change for a team repository.
Scope:
- Files: index.mjs, events/*.json, IAM policy JSON, deployment commands in docs
- Runtime: nodejs24.x
- Entry point: index.handler
Review for:
1. API Gateway event compatibility and response shape
2. Input validation and JSON parsing failures
3. Environment variables that overwrite existing values
4. IAM actions that are wider than needed
5. CloudWatch logs that expose secrets or personal data
6. AWS resources that can create unexpected cost
7. Local test commands that a reviewer can copy and run
Return:
- blocking issues
- non-blocking improvements
- exact commands to verify locally
- questions a human must answer before deploying
Follow the safe loop: explore, plan, edit, test locally, deploy only to dev, inspect logs, and ask a human to approve IAM and cost changes. The AWS deployment guide shows how to move from this CLI proof of concept toward CI/CD and IaC.
Common Pitfalls
The first mistake is deploying before local reproduction. If the event shape is wrong, a fixture catches it faster than CloudWatch.
The second mistake is putting secrets directly in environment variables. Environment variables are convenient for configuration, but secrets need separate access control and rotation.
The third mistake is wide IAM. dynamodb:* and Resource: "*" make demos pass, but they also expand the blast radius. Ask Claude Code to list every permission and justify it line by line.
The fourth mistake is exposing an API Gateway endpoint without an owner. Decide authentication, CORS, throttling, logging, and the cleanup date before sharing the URL.
The fifth mistake is ignoring cost. Lambda may look small, but API Gateway, CloudWatch Logs, DynamoDB, NAT, and external API calls can all create charges.
ClaudeCodeLab packages this kind of review into practical Claude Code templates and training material. If your team wants help designing AWS permissions, CLAUDE.md, review prompts, and deployment approval rules around a real repository, use the Claude Code consultation and training page.
Clean up proof-of-concept resources when you are done.
aws apigatewayv2 delete-api --api-id "$API_ID" --region "$AWS_REGION"
aws lambda delete-function --function-name "$FUNCTION_NAME" --region "$AWS_REGION"
aws iam delete-role-policy --role-name "$ROLE_NAME" --policy-name claude-orders-dev-inline
aws iam delete-role --role-name "$ROLE_NAME"
After trying the workflow in this article, the biggest practical gain was having Claude Code keep the handler, fixture, IAM draft, and CLI commands in one reviewable context. The unsafe part was not code generation; it was permission and exposure decisions. The reliable pattern is local fixture first, dev deployment second, CloudWatch evidence third, and human review for IAM and cost before anything reaches production.
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.