Fonctions serverless avec Claude Code : Lambda et Workers
Guide pratique pour creer des fonctions serverless avec Claude Code : plateforme, secrets, idempotence, tests et deploy.
Les fonctions serverless sont de petits morceaux de code executes pour chaque evenement ou requete HTTP, sans serveur applicatif a maintenir en permanence. Elles sont utiles pour les webhooks, les petites APIs, les traitements d’image ou de CSV, et les routes edge. Mais elles demandent tout de meme une vraie discipline sur les timeouts, retries, secrets, permissions, logs et couts.
Claude Code aide parce qu’il peut produire dans le meme contexte le handler, le fixture d’evenement, les tests, les notes de deploiement et la checklist de revue. Le bon modele n’est pas de le laisser deployer seul. Le bon modele est : clarifier les exigences, choisir la plateforme, reproduire localement l’evenement, separer configuration et secrets, concevoir l’idempotence, tester les echecs, puis faire valider exposition et couts par un humain.
Gardez les docs officielles a portee de main : AWS Lambda Documentation, Lambda avec Node.js, Cloudflare Workers development and testing et Workers get started guide. En complement, consultez le guide AWS Lambda, le guide Cloudflare Workers, le guide de developpement API et le guide de gestion des secrets.
Choisir Le Bon Cas D’usage
Le serverless fonctionne bien quand le travail est court, declenche par un evenement, et peut etre rejoue sans dommage.
| Cas d’usage | Pourquoi c’est adapte | Claude Code peut preparer | Revue humaine obligatoire |
|---|---|---|---|
| Webhook paiement ou formulaire | Une requete devient un evenement durable | Verification de signature, fixture, reponses d’erreur | Secrets, doublons, replay |
| Entree image resize ou CSV | Le travail lourd part vers storage ou queue | Validation, job ID, logs JSON | Taille fichiers, timeout, cleanup |
| Petite API JSON interne | Pas besoin d’un serveur permanent | Handler, tests, route | Auth, CORS, exposition, rate limit |
| Redirect/cache edge | Reponse proche de l’utilisateur | Route Worker, headers cache, notes rollout | Purge cache, donnees personnelles, SEO |
flowchart LR
A[Prompt d'exigences] --> B[Choisir Lambda ou Workers]
B --> C[Reproduire localement]
C --> D[Separer env et secrets]
D --> E[Idempotence et retry]
E --> F[Tests]
F --> G[Deploy dev]
G --> H[Logs et cleanup]
Prompt De Depart
Cree une fonction serverless minimale en Node.js.
Objectif:
- Gerer POST /orders et retourner une reponse accepted
- Fonctionner localement avec node local-test.mjs
- Supposer des evenements AWS Lambda HTTP API v2
Exigences:
- Expliquer index.mjs, events/create-order.json, local-test.mjs, index.test.mjs
- Retourner 400 si idempotency-key manque
- Retourner la meme reponse si idempotency-key est rejoue
- Distinguer invalid JSON, invalid input et unsupported route
- Logger en JSON sans secrets ni donnees personnelles
- Inclure une checklist avant deploiement
Contraintes:
- Aucun package npm externe
- En production, l'idempotence doit utiliser DynamoDB, KV ou un store durable
- IAM, URL publiques et ressources facturees exigent une confirmation humaine
Lambda Ou Workers ?
AWS Lambda est naturel pour les evenements AWS, IAM, S3, DynamoDB, SQS ou EventBridge. Cloudflare Workers est souvent meilleur pour l’HTTP edge : redirects, APIs legeres, cache, verification simple, KV/D1/R2. Vercel Functions est pratique dans une application Next.js, mais les exemples ci-dessous restent sur Lambda et Workers.
| Critere | AWS Lambda | Cloudflare Workers |
|---|---|---|
| Point fort | Integrations AWS, APIs metier, jobs async | HTTP edge, routing, cache, APIs legeres |
| Local | Node.js, SAM, AWS CLI | Wrangler |
| Permissions | Role et policy IAM | Bindings, secrets, permissions compte |
| Risque | IAM trop large, cout VPC/NAT, volume logs | Drift des bindings, limites runtime, KV consistency |
Handler Lambda Executable Localement
// 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);
}
{
"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\":\"EUR\"}",
"isBase64Encoded": false
}
// 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
La Map n’est qu’une demo locale. En production, utilisez une ecriture conditionnelle DynamoDB, une contrainte unique en base, Cloudflare KV/D1 ou un autre stockage durable.
Tests
// 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: "EUR" }),
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
Version Workers Avec KV
// 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 });
},
};
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
Pieges Et Checklist
Premier piege : croire a une execution exactement une fois. Les webhooks, queues, evenements async et navigateurs peuvent rejouer. Deuxieme piege : laisser des secrets dans les logs ou exemples. Troisieme piege : accepter des permissions trop larges comme Resource: "*". Quatrieme piege : publier une URL sans proprietaire, auth, CORS, rate limit, retention de logs et plan de suppression.
| Check | A verifier |
|---|---|
| Exigences | Input, output, owner et erreurs sont documentes |
| Runtime | Lambda Node.js runtime ou Workers compatibility_date est explicite |
| Local | Fixture et node --test passent |
| Env/secrets | Config et secrets sont separes |
| Idempotence | Un retry ne cree pas de doublon |
| Timeout/retry | Le travail lent va vers queue ou job durable |
| Observabilite | Logs JSON, taux d’erreur, alertes et retention sont definis |
| Cleanup | Commandes de suppression ou etapes dashboard sont documentees |
zip function.zip index.mjs
aws lambda update-function-code \
--function-name serverless-orders-dev \
--zip-file fileb://function.zip
npx wrangler deploy
Prompt final de revue :
Relis cette fonction serverless avant publication.
Separe blocking issues, non-blocking improvements et human confirmations.
Controle idempotence, timeout/retry, secrets, IAM ou bindings, logs,
reproductibilite locale, cleanup, liens officiels et liens internes.
ClaudeCodeLab rassemble ces pratiques dans des produits et templates Claude Code. Pour mettre en place permissions AWS, CLAUDE.md, prompts de revue et validation de deploiement dans une equipe, consultez la page formation et consultation Claude Code.
Dans l’essai pratique, le gain le plus net venait du fixture d’evenement cree en premier. Claude Code va vite, mais les retries, secrets et etapes de suppression deviennent fiables seulement si ces contraintes sont explicites des le premier prompt.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.