{
  "id": "build-a-cloudflare-worker-api-proxy",
  "type": "prompts",
  "category": "prompts",
  "locale": "en",
  "url": "/prompts/build-a-cloudflare-worker-api-proxy",
  "title": "Prompt to Build a Cloudflare Worker API Proxy",
  "description": "Copy-paste AI prompt to build a Cloudflare Worker that proxies and rate-limits external API calls, adds auth headers, and caches responses.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Cloudflare",
    "TypeScript"
  ],
  "tags": [
    "cloudflare",
    "typescript",
    "security",
    "deploy"
  ],
  "difficulty": "medium",
  "updated": "2026-06-08",
  "markdown": "Use this prompt to build a Cloudflare Worker that sits in front of an external API,\ninjects auth headers, rate-limits by IP using Workers KV, and caches responses —\nso clients never see your upstream API key.\n\n## Main Prompt\n\n```txt title=\"Main Prompt\"\nYou are building a Cloudflare Worker using TypeScript and the Workers runtime (not Node.js).\nThe Worker will proxy requests to an upstream API (e.g., OpenAI at https://api.openai.com).\n\nTask: create a production-ready API proxy Worker.\n\nRequirements:\n- Use `wrangler` v3 for local dev. Scaffold with `bun create cloudflare@latest` and choose\n  \"Hello World\" Worker with TypeScript.\n- Upstream URL: read from a Wrangler secret `UPSTREAM_URL` (string).\n- Auth: inject `Authorization: Bearer ${env.UPSTREAM_API_KEY}` on every proxied request,\n  where `UPSTREAM_API_KEY` is a Wrangler secret. Never expose this header to the client.\n- CORS: allow only origins in `env.ALLOWED_ORIGINS` (comma-separated string secret).\n  Return a `403` for disallowed origins. Handle preflight OPTIONS requests.\n- Rate limiting: use Workers KV binding `RATE_LIMIT_KV`.\n  - Key: `rl:${ip}` where ip is `request.headers.get('CF-Connecting-IP')`.\n  - Value: request count for the current UTC minute (TTL = 60 s).\n  - Limit: 60 requests/minute per IP. Return `429` with `Retry-After: 60` if exceeded.\n- Caching: for GET requests, check `caches.default` before proxying upstream. Cache\n  successful responses with `Cache-Control: public, max-age=300`.\n- Strip the following headers from the upstream response before returning to the client:\n  `x-powered-by`, `server`, `cf-ray`.\n- Wrangler config: declare the KV namespace binding and all secrets in `wrangler.toml`.\n- Do NOT use Node.js APIs (`fs`, `path`, `Buffer`) — Workers runtime only.\n\nStop and list all planned files before writing code.\n```\n\n## Implementation Notes\n\n- Cloudflare Workers receive a `Request` and return a `Response` — avoid `express`-style patterns.\n- `caches.default` is the Cloudflare edge cache; it only works in production. Use `MINIFLARE_CACHE`\n  for local dev testing or mock the cache.\n- `CF-Connecting-IP` is injected by Cloudflare — it is not spoofable from the public internet, but\n  test locally with a hardcoded fallback IP.\n- Wrangler secrets are set with `wrangler secret put UPSTREAM_API_KEY` — never store them in\n  `wrangler.toml` or committed `.env` files.\n\n## Expected File Changes\n\n```txt\nwrangler.toml                  (new)\nsrc/index.ts                   (new — Worker entrypoint)\nsrc/cors.ts                    (new — CORS helper)\nsrc/rate-limit.ts              (new — KV rate limiter)\npackage.json                   (new)\ntsconfig.json                  (new)\n.dev.vars                      (new — local dev secrets, gitignored)\n.gitignore                     (edited — add .dev.vars)\n```\n\n## Acceptance Criteria\n\n- `wrangler dev` starts without errors and proxies a `GET /` to the upstream URL.\n- An IP sending 61 requests in one minute receives a `429` on the 61st request.\n- A request from a non-allowed origin receives a `403`.\n- The `Authorization` header does not appear in the response or in any client-visible header.\n- `wrangler deploy` succeeds and the Worker is live on `workers.dev`.\n\n## Test Commands\n\n```bash\nwrangler dev &\n# test normal proxy\ncurl http://localhost:8787/ -H \"Origin: https://myapp.com\"\n# test CORS rejection\ncurl http://localhost:8787/ -H \"Origin: https://evil.com\"\n# test rate limit (requires 61 rapid requests)\nfor i in $(seq 1 62); do curl -s -o /dev/null -w \"%{http_code}\\n\" http://localhost:8787/; done\n```\n\n## Common AI Mistakes\n\n- Using `process.env` instead of the `env` parameter passed to the Worker `fetch` handler.\n- Forgetting to handle `OPTIONS` preflight requests, breaking CORS for POST/PUT calls.\n- Storing `UPSTREAM_API_KEY` in `wrangler.toml` as a plain variable instead of a secret.\n- Using `node:buffer` or other Node.js built-ins, which are not available in the Workers runtime.\n\n## Fix Prompt\n\n```txt title=\"Fix Prompt\"\nThe Worker fails with a runtime error or leaks the API key. Fix in order:\n1. Replace `process.env.X` with `env.X` everywhere — Workers use the `env` handler parameter.\n2. Add an OPTIONS handler before the proxy logic that returns the CORS headers with a 204 status.\n3. Move `UPSTREAM_API_KEY` from `wrangler.toml` [vars] to a secret: `wrangler secret put UPSTREAM_API_KEY`.\nShow only the corrected diff.\n```"
}