Use Cases (Updated: 6/1/2026)

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.

Claude Code Serverless Functions Guide: Lambda, Workers, Tests

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 caseWhy it fitsAsk Claude Code to draftHuman review must cover
Payment or form webhookOne request can become one durable eventSignature check, fixture, failure responsesSecrets, duplicate events, replay rules
Image resize or CSV import entry pointHeavy work can move to storage or a queueInput validation, job ID, structured logsFile limits, timeout, safe deletion
Internal JSON APINo persistent app server for small endpointsHandler, tests, API routeAuth, CORS, public exposure, throttling
Edge redirects and cache rulesFast response near the userWorker route, cache headers, rollout notesCache 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.

DecisionAWS LambdaCloudflare Workers
Best fitAWS integrations, business APIs, async jobsEdge HTTP, routing, cache, lightweight APIs
Local workflowNode.js, SAM, AWS CLIWrangler local development
PermissionsIAM role and policyBindings, secrets, account permissions
Common riskOverbroad IAM, VPC/NAT cost, log volumeBinding 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:

CheckWhat to confirm
RequirementsInputs, outputs, owner, and failure responses are written down
RuntimeLambda Node.js runtime or Workers compatibility date is explicit
Local proofFixture and node --test pass
Env/secretsConfig and secrets are separated
IdempotencyRetried requests cannot double-charge or double-create
Timeout/retrySlow work moves to a queue or durable job
ObservabilityJSON logs, error rate, alerts, and retention are defined
CleanupDelete 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.

#Claude Code #serverless functions #AWS Lambda #Cloudflare Workers #Vercel
Free

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.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.