Use Cases (업데이트: 2026. 6. 3.)

Claude Code로 Cloudflare Workers API 구현하기

Claude Code로 Workers API, Wrangler, KV/D1/R2, 캐시, 레이트 리밋, 로그, 보안 헤더를 구현합니다.

Claude Code로 Cloudflare Workers API 구현하기

Cloudflare Workers는 JavaScript/TypeScript를 Cloudflare의 엣지 네트워크에서 실행하는 런타임입니다. 엣지는 사용자의 가까운 위치에서 코드를 실행한다는 뜻입니다. 작은 API, Webhook, BFF, 캐시 응답, 보안 검사, R2 파일 게이트웨이에 잘 맞습니다.

Claude Code와 잘 맞는 이유는 구조가 명확하기 때문입니다. Worker는 fetch handler, wrangler.toml, 그리고 KV/D1/R2 같은 binding으로 나뉩니다. 2026년 6월 3일 공식 문서를 다시 확인했습니다. 구현 전에는 Workers, Wrangler, bindings, KV, D1, R2, Cache API, Rate Limiting, Workers Logs를 함께 확인하세요. 관련 글로는 serverless functionsAWS Lambda guide가 있습니다.

만들 API

예제는 주문 API입니다. D1에 주문을 저장하고, KV에서 설정을 읽고, R2에 receipt JSON을 저장하고, 안전한 GET 응답만 짧게 캐시합니다. 인증된 요청은 Rate Limiting으로 제한하고 모든 응답에 보안 헤더를 붙입니다.

flowchart LR
  Client["Client"] --> Worker["Worker fetch handler"]
  Worker --> D1["D1 orders"]
  Worker --> KV["KV settings"]
  Worker --> R2["R2 receipts"]
  Worker --> Cache["Cache API"]
  Worker --> Logs["Workers Logs"]

binding은 Worker에 외부 리소스를 연결하는 이름입니다. binding = "DB"라고 선언하면 코드에서는 env.DB로 D1을 사용합니다.

Claude Code 프롬프트

Cloudflare Workers + TypeScript로 주문 API를 구현하세요.

파일:
- src/index.ts
- wrangler.toml
- schema.sql

요구사항:
- module fetch handler 형식 사용
- GET /health는 JSON 반환
- GET /orders/:id는 D1에서 1건 조회하고 안전한 공개 응답만 30초 캐시
- POST /orders는 JSON 검증 후 D1에 저장
- Authorization Bearer 토큰을 API_TOKEN과 비교
- SETTINGS KV, DB D1, RECEIPTS R2, API_RATE_LIMITER binding 사용
- 모든 응답에 security headers 추가
- console.log는 JSON 객체로 출력
- curl 검증 명령 포함

금지:
- secret 값을 wrangler.toml에 쓰지 않기
- 장시간 실행되는 Node.js server를 전제하지 않기
- 의사코드로 끝내지 않기

Wrangler 설정

npm create cloudflare@latest claude-worker-api -- --type=hello-world
cd claude-worker-api
npm install -D typescript wrangler
npx wrangler --version

C3는 새 프로젝트에서 wrangler.jsonc를 만들 수 있습니다. Wrangler는 JSON/JSONC와 TOML 설정 파일을 모두 지원합니다. 이 글은 읽기 쉽게 TOML로 보여 주며, 프로젝트가 wrangler.jsonc를 쓰고 있다면 같은 키를 JSONC 형태로 유지하면 됩니다.

name = "claude-worker-api"
main = "src/index.ts"
compatibility_date = "2026-06-03"

[vars]
PUBLIC_ENV = "production"

[observability]
enabled = true
head_sampling_rate = 1

[[d1_databases]]
binding = "DB"
database_name = "claude-worker-api"
database_id = "replace-with-d1-database-id"

[[kv_namespaces]]
binding = "SETTINGS"
id = "replace-with-kv-namespace-id"

[[r2_buckets]]
binding = "RECEIPTS"
bucket_name = "claude-worker-receipts"

[[ratelimits]]
name = "API_RATE_LIMITER"
namespace_id = "1001"

  [ratelimits.simple]
  limit = 60
  period = 60
npx wrangler login
npx wrangler d1 create claude-worker-api
npx wrangler kv namespace create SETTINGS
npx wrangler r2 bucket create claude-worker-receipts
npx wrangler secret put API_TOKEN

D1 스키마

CREATE TABLE IF NOT EXISTS orders (
  id TEXT PRIMARY KEY,
  email TEXT NOT NULL,
  amount INTEGER NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending',
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_orders_email ON orders(email);
npx wrangler d1 execute claude-worker-api --local --file=./schema.sql
npx wrangler d1 execute claude-worker-api --remote --file=./schema.sql

실행 가능한 Worker

export interface Env {
  API_TOKEN: string;
  PUBLIC_ENV: string;
  DB: D1Database;
  SETTINGS: KVNamespace;
  RECEIPTS: R2Bucket;
  API_RATE_LIMITER: RateLimit;
}

const securityHeaders = {
  "content-security-policy": "default-src 'none'; frame-ancestors 'none'",
  "x-content-type-options": "nosniff",
  "x-frame-options": "DENY",
  "referrer-policy": "no-referrer",
  "permissions-policy": "camera=(), microphone=(), geolocation=()",
};

function json(data: unknown, init: ResponseInit = {}) {
  return new Response(JSON.stringify(data), {
    ...init,
    headers: {
      "content-type": "application/json; charset=utf-8",
      ...securityHeaders,
      ...init.headers,
    },
  });
}

export default {
  async fetch(request, env, ctx): Promise<Response> {
    const url = new URL(request.url);
    const requestId = crypto.randomUUID();
    console.log({ event: "request_started", requestId, method: request.method, path: url.pathname });

    if (url.pathname === "/health") {
      const maintenance = await env.SETTINGS.get("maintenance");
      return json({ ok: true, env: env.PUBLIC_ENV, maintenance: maintenance === "true" });
    }

    if (request.headers.get("authorization") !== `Bearer ${env.API_TOKEN}`) {
      return json({ error: "unauthorized" }, { status: 401 });
    }

    const { success } = await env.API_RATE_LIMITER.limit({
      key: request.headers.get("authorization")?.slice(-16) ?? "anonymous",
    });
    if (!success) return json({ error: "rate_limited" }, { status: 429 });

    const match = url.pathname.match(/^\/orders\/([a-zA-Z0-9_-]+)$/);

    if (request.method === "GET" && match) {
      const cacheKey = new Request(url.toString(), { method: "GET" });
      const cached = await caches.default.match(cacheKey);
      if (cached) return cached;

      const order = await env.DB.prepare(
        "SELECT id, email, amount, status, created_at FROM orders WHERE id = ?"
      ).bind(match[1]).first();
      if (!order) return json({ error: "not_found" }, { status: 404 });

      const response = json({ order }, {
        headers: { "cache-control": "public, max-age=30", "cache-tag": `order-${match[1]}` },
      });
      ctx.waitUntil(caches.default.put(cacheKey, response.clone()));
      return response;
    }

    if (request.method === "POST" && url.pathname === "/orders") {
      const body = await request.json<{ email?: string; amount?: number }>();
      if (!body.email?.includes("@") || !Number.isInteger(body.amount) || body.amount <= 0) {
        return json({ error: "invalid_order" }, { status: 400 });
      }

      const id = crypto.randomUUID();
      await env.DB.prepare(
        "INSERT INTO orders (id, email, amount, status) VALUES (?, ?, ?, ?)"
      ).bind(id, body.email, body.amount, "pending").run();

      await env.RECEIPTS.put(`orders/${id}.json`, JSON.stringify({ id, email: body.email, amount: body.amount }), {
        httpMetadata: { contentType: "application/json" },
      });

      console.log({ event: "order_created", requestId, orderId: id });
      return json({ id, status: "pending" }, { status: 201 });
    }

    return json({ error: "not_found" }, { status: 404 });
  },
} satisfies ExportedHandler<Env>;

로컬 검증과 배포

printf "API_TOKEN=dev-token\n" > .dev.vars
npx wrangler dev
curl http://localhost:8787/health

curl -X POST http://localhost:8787/orders \
  -H "authorization: Bearer dev-token" \
  -H "content-type: application/json" \
  -d "{\"email\":\"masa@example.com\",\"amount\":1200}"

curl http://localhost:8787/orders/replace-with-created-id \
  -H "authorization: Bearer dev-token"
npx wrangler secret put API_TOKEN
npx wrangler d1 execute claude-worker-api --remote --file=./schema.sql
npx wrangler deploy
npx wrangler tail

실제 사용 사례

첫째, 문의 폼이나 주문 API입니다. D1에는 상태를, R2에는 receipt나 첨부 파일을, Logs에는 운영 흔적을 남깁니다.

둘째, Webhook 수신기입니다. 서명을 검증하고 event ID를 D1에 저장해 중복 처리를 막습니다.

셋째, BFF입니다. BFF는 프런트엔드 전용 얇은 백엔드입니다. Worker가 API key를 숨기고 응답을 UI에 맞게 정리하며 안전한 응답만 캐시합니다.

넷째, R2 파일 다운로드 제어입니다. 큰 파일은 R2에 두고 인증과 로그는 Worker가 담당합니다.

주의할 점

Workers를 장시간 실행되는 Node.js server처럼 작성하지 마세요. Web API와 binding을 기준으로 설계해야 합니다.

KV, D1, R2, Cache API의 역할을 섞지 마세요. KV는 작은 key-value, D1은 관계형 데이터, R2는 객체, Cache API는 짧은 응답 캐시에 적합합니다.

개인화 응답을 공유 캐시에 넣지 마세요. cookie, token, email, 사용자별 결과가 들어가면 캐시 전략을 다시 설계해야 합니다.

IP만으로 rate limit하지 마세요. API key, user ID, tenant ID 같은 안정적인 식별자가 낫습니다.

플랫폼 선택 기준

Cloudflare cache와 binding에 가까운 저지연 HTTP API라면 Workers가 좋습니다. Cloudflare Pages 사이트 옆에 약간의 동적 로직을 붙일 때는 Pages Functions가 자연스럽습니다. 컨테이너, 긴 작업, 기존 프레임워크가 필요하면 Cloud Run을 고려하세요. S3, DynamoDB, EventBridge, SQS 중심이라면 Lambda가 더 맞습니다.

Claude Code 리뷰 프롬프트

이 Cloudflare Workers 구현을 리뷰하세요.

확인:
- Workers runtime 호환성
- Env 타입과 wrangler binding 이름 일치
- secret이 코드, 로그, 설정에 노출되는지
- D1 bind 사용과 SQL injection 위험
- 안전하지 않은 Cache API 사용
- Rate Limiting key 선택
- 모든 응답의 security headers
- 누락된 curl 검증 단계

심각도순 지적, 수정안, 테스트를 출력하세요.

CTA와 검증 메모

처음에는 /health, 읽기 전용 GET, Webhook 중 하나만 옮기세요. Claude Code에는 파일 범위, binding 이름, 검증 명령, 금지 사항을 함께 전달합니다. 재사용 가능한 템플릿은 products에서, 팀 교육과 리뷰 습관은 training에서 확인하세요.

실제로 해본 결과: 이 글의 예제는 Wrangler, D1, Worker, curl 흐름으로 점검했지만, 배포 전에는 본인 Cloudflare 계정에서 wrangler deploy, wrangler tail, 보안 헤더, 캐시, 429 응답을 다시 확인하세요.

#Claude Code #Cloudflare Workers #edge computing #serverless #API
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.