Claude Code Serverless Functions Guide: Lambda, Workers, Tests
Build reliable serverless functions with Claude Code: platform choice, env vars, idempotency, retries, tests, and deploy checks.
Serverless functions are short-lived pieces of code that run for each event or HTTP request without you managing always-on servers. They are excellent for webhooks, small APIs, image or CSV entry points, and edge routing. They are also easy to ship badly if you ignore timeouts, retries, secrets, IAM, and cleanup.
Claude Code helps because it can keep the handler, event fixture, tests, deployment notes, and review checklist in one working context. The safe pattern is not “let AI deploy it.” The safe pattern is: write the requirements, choose the runtime, reproduce the event locally, separate config from secrets, design idempotency, test failure cases, then deploy only after a human reviews exposure and cost.
Keep the official docs nearby: AWS Lambda Documentation, Building Lambda functions with Node.js, Cloudflare Workers development and testing, and the Cloudflare Workers get started guide. For related implementation details, continue with the internal AWS Lambda guide, Cloudflare Workers guide, API development guide, and secrets management guide.
Start With the Use Case
Serverless is strongest when the work is short, event-shaped, and easy to retry safely.
| Use case | Why it fits | Ask Claude Code to draft | Human review must cover |
|---|---|---|---|
| Payment or form webhook | One request can become one durable event | Signature check, fixture, failure responses | Secrets, duplicate events, replay rules |
| Image resize or CSV import entry point | Heavy work can move to storage or a queue | Input validation, job ID, structured logs | File limits, timeout, safe deletion |
| Internal JSON API | No persistent app server for small endpoints | Handler, tests, API route | Auth, CORS, public exposure, throttling |
| Edge redirects and cache rules | Fast response near the user | Worker route, cache headers, rollout notes | Cache purge, personal data, SEO impact |
flowchart LR
A[Write requirements prompt] --> B[Choose Lambda or Workers]
B --> C[Reproduce event locally]
C --> D[Separate env and secrets]
D --> E[Design idempotency and retries]
E --> F[Run tests]
F --> G[Deploy to dev]
G --> H[Inspect logs and cleanup path]
Prompt Claude Code Like a Reviewer
Give Claude Code the shape of the work, not just the feature name.
Create a minimal Node.js serverless function.
Goal:
- Handle POST /orders and return an accepted order response
- Run locally with node local-test.mjs
- Assume AWS Lambda HTTP API v2 events
Requirements:
- Explain index.mjs, events/create-order.json, local-test.mjs, and index.test.mjs
- Return 400 when idempotency-key is missing
- Return the same response when the same idempotency-key is repeated
- Separate invalid JSON, invalid input, and unsupported route responses
- Log JSON without secrets or personal data
- Include a deployment checklist
Constraints:
- No external npm packages
- Production idempotency should use DynamoDB, KV, or another durable store
- IAM, public URLs, and billable resources require human confirmation
This makes Claude Code produce a reviewable unit: code, fixtures, tests, and operational checks.
Choose the Platform
Use AWS Lambda when your function needs AWS events, IAM, S3, DynamoDB, SQS, EventBridge, or private backend integration. Use Cloudflare Workers when the core job is HTTP at the edge: redirects, lightweight APIs, cache policy, bot checks, or KV/D1/R2-backed flows. Vercel Functions are useful when the function belongs inside a Next.js application, but the examples below focus on Lambda and Workers because the official concepts are easy to verify.
| Decision | AWS Lambda | Cloudflare Workers |
|---|---|---|
| Best fit | AWS integrations, business APIs, async jobs | Edge HTTP, routing, cache, lightweight APIs |
| Local workflow | Node.js, SAM, AWS CLI | Wrangler local development |
| Permissions | IAM role and policy | Bindings, secrets, account permissions |
| Common risk | Overbroad IAM, VPC/NAT cost, log volume | Binding drift, runtime limits, KV consistency |
Runnable Lambda Handler
Start locally. This dependency-free handler accepts an HTTP API style event, enforces an idempotency-key, and returns the same result for repeated local calls.
// index.mjs
import crypto from "node:crypto";
const localIdempotencyStore = new Map();
function json(statusCode, body) {
return {
statusCode,
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
};
}
function readHeader(headers = {}, name) {
const target = name.toLowerCase();
const found = Object.entries(headers).find(([key]) => key.toLowerCase() === target);
return found?.[1];
}
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 method = event.requestContext?.http?.method ?? event.httpMethod ?? "GET";
const path = event.rawPath ?? event.path ?? "/";
const requestId = context.awsRequestId ?? crypto.randomUUID();
console.log(JSON.stringify({ level: "info", message: "request.start", requestId, method, path }));
if (method !== "POST" || path !== "/orders") {
return json(404, { error: "not_found" });
}
const idempotencyKey = readHeader(event.headers, "idempotency-key");
if (!idempotencyKey) {
return json(400, { error: "idempotency_key_required" });
}
if (localIdempotencyStore.has(idempotencyKey)) {
return json(200, { ...localIdempotencyStore.get(idempotencyKey), replay: true });
}
let body;
try {
body = parseBody(event);
} catch {
return json(400, { error: "invalid_json" });
}
if (!Number.isFinite(body.amount) || body.amount <= 0 || typeof body.currency !== "string") {
return json(400, { error: "invalid_order" });
}
const accepted = {
orderId: crypto.randomUUID(),
status: "accepted",
amount: body.amount,
currency: body.currency,
};
localIdempotencyStore.set(idempotencyKey, accepted);
console.log(JSON.stringify({ level: "info", message: "order.accepted", requestId, orderId: accepted.orderId }));
return json(202, accepted);
}
Create the fixture:
{
"version": "2.0",
"routeKey": "POST /orders",
"rawPath": "/orders",
"headers": {
"content-type": "application/json",
"idempotency-key": "demo-key-001"
},
"requestContext": {
"http": {
"method": "POST",
"path": "/orders"
}
},
"body": "{\"amount\":3200,\"currency\":\"USD\"}",
"isBase64Encoded": false
}
Run it locally:
// local-test.mjs
import { readFile } from "node:fs/promises";
import { handler } from "./index.mjs";
const eventPath = process.argv[2] ?? "events/create-order.json";
const event = JSON.parse(await readFile(eventPath, "utf8"));
const first = await handler(event, { awsRequestId: "local-001" });
const second = await handler(event, { awsRequestId: "local-002" });
console.log("first:", first.statusCode, first.body);
console.log("second:", second.statusCode, second.body);
node local-test.mjs events/create-order.json
The in-memory Map is only a local demo. In production, use DynamoDB conditional writes, a database unique key, Cloudflare KV/D1, or another durable store. Lambda execution environments can disappear or be reused, so memory is not your source of truth.
Tests Before Deploy
// index.test.mjs
import crypto from "node:crypto";
import test from "node:test";
import assert from "node:assert/strict";
import { handler } from "./index.mjs";
function event(overrides = {}) {
return {
rawPath: "/orders",
headers: { "idempotency-key": crypto.randomUUID() },
requestContext: { http: { method: "POST" } },
body: JSON.stringify({ amount: 1200, currency: "USD" }),
isBase64Encoded: false,
...overrides,
};
}
test("requires idempotency-key", async () => {
const result = await handler(event({ headers: {} }), {});
assert.equal(result.statusCode, 400);
});
test("accepts a valid order", async () => {
const result = await handler(event(), {});
assert.equal(result.statusCode, 202);
assert.equal(JSON.parse(result.body).status, "accepted");
});
test("rejects invalid JSON", async () => {
const result = await handler(event({ body: "not-json" }), {});
assert.equal(result.statusCode, 400);
});
node --test index.test.mjs
Workers Version With Bindings
Workers use fetch(request, env). Store the idempotency result in KV and keep the webhook secret in a Worker secret.
// src/worker.js
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method !== "POST" || url.pathname !== "/orders") {
return Response.json({ error: "not_found" }, { status: 404 });
}
if (request.headers.get("x-webhook-secret") !== env.WEBHOOK_SECRET) {
return Response.json({ error: "unauthorized" }, { status: 401 });
}
const idempotencyKey = request.headers.get("idempotency-key");
if (!idempotencyKey) {
return Response.json({ error: "idempotency_key_required" }, { status: 400 });
}
const existing = await env.IDEMPOTENCY_KV.get(idempotencyKey, "json");
if (existing) {
return Response.json({ ...existing, replay: true });
}
const body = await request.json();
if (!Number.isFinite(body.amount) || typeof body.currency !== "string") {
return Response.json({ error: "invalid_order" }, { status: 400 });
}
const accepted = {
orderId: crypto.randomUUID(),
status: "accepted",
amount: body.amount,
currency: body.currency,
};
await env.IDEMPOTENCY_KV.put(idempotencyKey, JSON.stringify(accepted), {
expirationTtl: 86400,
});
return Response.json(accepted, { status: 202 });
},
};
{
"name": "serverless-orders-worker",
"main": "src/worker.js",
"compatibility_date": "2026-06-01",
"kv_namespaces": [
{
"binding": "IDEMPOTENCY_KV",
"id": "replace_with_real_kv_namespace_id"
}
]
}
npm create cloudflare@latest serverless-orders-worker
cd serverless-orders-worker
npx wrangler kv namespace create IDEMPOTENCY_KV
npx wrangler secret put WEBHOOK_SECRET
npx wrangler dev
Pitfalls and Deployment Checklist
The first pitfall is assuming exactly-once execution. Webhook providers, queues, async Lambda events, and browsers can retry. Store an external idempotency key and return the same result for repeats.
The second pitfall is hiding secrets in logs or examples. Use AWS Secrets Manager, Parameter Store, or Worker secrets; keep real values out of articles, PRs, and test fixtures.
The third pitfall is overbroad permission. If Claude Code proposes Resource: "*", ask it to justify every action and resource. Passing a demo is not the same as safe production access.
The fourth pitfall is deploying a public endpoint without an owner. Decide authentication, CORS, rate limits, log retention, rollback, and deletion date before sharing the URL.
Before deploying, verify:
| Check | What to confirm |
|---|---|
| Requirements | Inputs, outputs, owner, and failure responses are written down |
| Runtime | Lambda Node.js runtime or Workers compatibility date is explicit |
| Local proof | Fixture and node --test pass |
| Env/secrets | Config and secrets are separated |
| Idempotency | Retried requests cannot double-charge or double-create |
| Timeout/retry | Slow work moves to a queue or durable job |
| Observability | JSON logs, error rate, alerts, and retention are defined |
| Cleanup | Delete commands or dashboard cleanup steps are documented |
Minimal update commands:
zip function.zip index.mjs
aws lambda update-function-code \
--function-name serverless-orders-dev \
--zip-file fileb://function.zip
npx wrangler deploy
End with a Claude Code review prompt:
Review this serverless function before publication.
Separate blocking issues, non-blocking improvements, and human confirmations.
Check idempotency, timeout/retry behavior, secrets, IAM or bindings, logs,
local test reproducibility, cleanup steps, official links, and internal links.
ClaudeCodeLab packages these patterns into Claude Code products and templates. For a team rollout around AWS permissions, CLAUDE.md, review prompts, and deployment approval rules, use the Claude Code consultation and training page.
After trying this workflow, the biggest gain was creating the event fixture first. Claude Code can move quickly, but it only handles retries, secrets, and cleanup well when those constraints are explicit. Local reproduction, idempotency, logs, and deletion steps should be in the first prompt, not added after the function is already live.
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.