Use Cases (Updated: 6/2/2026)

Complete CORS Configuration Guide with Claude Code

Implement safe CORS with Claude Code: preflight, credentials, origin allowlists, tests, and review prompts.

Complete CORS Configuration Guide with Claude Code

Configure CORS Correctly With Claude Code

Running a frontend on localhost:3000 and an API on localhost:8787 is enough to trigger a CORS error. The tempting fix is to add Access-Control-Allow-Origin: *, but that becomes unsafe as soon as the API uses cookies, authorization headers, or an admin dashboard.

CORS, or Cross-Origin Resource Sharing, is the browser-controlled mechanism that lets a server decide which other origins may read its responses. An origin is the combination of scheme, host, and port. https://app.example.com, https://api.example.com, http://localhost:3000, and http://localhost:5173 are all different origins.

This guide breaks the work into reviewable pieces so Claude Code can help without hiding the security decisions. You will get copy-pasteable examples for Express, Fastify, Cloudflare Workers, and a Next.js Route Handler, plus preflight checks, credential rules, allowlist patterns, test commands, and Claude Code review prompts.

The key mindset shift is that CORS is not authentication. It controls whether browser JavaScript can read a cross-origin response. It does not stop curl, server-to-server calls, or unauthorized users. Keep API authentication, authorization, CSRF protection, rate limiting, and security headers as separate controls.

sequenceDiagram
  participant Browser as Browser
  participant API as API server
  Browser->>API: OPTIONS /api/messages<br/>Origin + Access-Control-Request-*
  API-->>Browser: 204 + Access-Control-Allow-*
  Browser->>API: POST /api/messages<br/>Cookie or Authorization
  API-->>Browser: 200 + Access-Control-Allow-Origin

Decisions Before Code

Decide these values before asking Claude Code to write CORS middleware. Vague prompts often produce permissive examples that are fine for demos but risky in production.

DecisionExampleWatch for
Allowed originshttps://app.example.com, https://admin.example.comNo paths and no trailing slash
CredentialsCookie, Authorization headerCookie flows also need SameSite=None; Secure
MethodsGET,POST,PUT,PATCH,DELETE,OPTIONSAllow only what the API uses
Request headersContent-Type,Authorization,X-Request-IDMust match the preflight request

A preflight request is the browser’s permission check before the real request. For JSON POST, Authorization, PUT, DELETE, and many custom headers, the browser first sends OPTIONS. If the response does not include matching Access-Control-Allow-Methods and Access-Control-Allow-Headers, the browser never sends the real request.

Express Configuration

This example targets Node.js 20 or later. The official Express cors middleware accepts a function for origin, so we can evaluate each request against an allowlist. Because the API supports credentials, it reflects only allowed origins and sets credentials: true.

npm init -y
npm install express cors
node server.mjs
// server.mjs
import express from "express";
import cors from "cors";

const app = express();

const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
  "http://localhost:5173",
]);

function isAllowedOrigin(origin) {
  if (!origin) return true;
  if (allowedOrigins.has(origin)) return true;
  return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}

const corsOptions = {
  origin(origin, callback) {
    callback(null, isAllowedOrigin(origin));
  },
  credentials: true,
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
  allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
  exposedHeaders: ["X-Request-ID"],
  maxAge: 86400,
  optionsSuccessStatus: 204,
};

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (origin && !isAllowedOrigin(origin)) {
    return res.status(403).json({ error: "Origin not allowed" });
  }
  next();
});

app.use(cors(corsOptions));
app.use(express.json());

app.get("/api/health", (_req, res) => {
  res.setHeader("X-Request-ID", crypto.randomUUID());
  res.json({ ok: true });
});

app.post("/api/messages", (req, res) => {
  res.setHeader("X-Request-ID", crypto.randomUUID());
  res.json({ ok: true, received: req.body });
});

app.listen(8787, () => {
  console.log("API listening on http://localhost:8787");
});

In production, run with NODE_ENV=production and keep only real application origins in allowedOrigins. Requests without an Origin header are not browser CORS requests, so this sample lets them through; API keys, JWT checks, and user permissions still belong in normal authentication middleware.

Fastify Configuration

Fastify uses @fastify/cors. Its official README supports booleans, strings, arrays, regular expressions, and functions for origin, but a Set-based exact match is easier to audit. Avoid broad regular expressions unless you have a strong reason.

npm init -y
npm install fastify @fastify/cors
node server.mjs
// server.mjs
import Fastify from "fastify";
import cors from "@fastify/cors";

const app = Fastify({ logger: true });

const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
  "http://localhost:5173",
]);

function isAllowedOrigin(origin) {
  if (!origin) return true;
  if (allowedOrigins.has(origin)) return true;
  return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}

app.addHook("onRequest", async (request, reply) => {
  const origin = request.headers.origin;
  if (origin && !isAllowedOrigin(origin)) {
    return reply.code(403).send({ error: "Origin not allowed" });
  }
});

await app.register(cors, {
  origin(origin, callback) {
    callback(null, isAllowedOrigin(origin));
  },
  credentials: true,
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
  allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
  exposedHeaders: ["X-Request-ID"],
  maxAge: 86400,
  strictPreflight: true,
});

app.get("/api/health", async () => ({ ok: true }));

app.post("/api/messages", async (request) => {
  return { ok: true, received: request.body };
});

await app.listen({ port: 8787, host: "0.0.0.0" });

Plugin and hook order matters in Fastify. If an auth hook rejects OPTIONS before the CORS plugin can answer, the browser will block the real request. Ask Claude Code to inspect plugin registration order, not just header names.

Cloudflare Workers Configuration

Cloudflare Workers expose the standard Fetch API. Handle OPTIONS explicitly, return CORS headers on success and error responses, and add Vary: Origin when the response differs by origin.

// src/index.ts
const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
]);

function getCorsHeaders(request: Request): HeadersInit | null {
  const origin = request.headers.get("Origin");
  if (!origin) return {};
  if (!allowedOrigins.has(origin)) return null;

  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
    "Access-Control-Max-Age": "86400",
    "Vary": "Origin",
  };
}

export default {
  async fetch(request: Request): Promise<Response> {
    const corsHeaders = getCorsHeaders(request);
    if (corsHeaders === null) {
      return Response.json({ error: "Origin not allowed" }, { status: 403 });
    }

    if (request.method === "OPTIONS") {
      return new Response(null, { status: 204, headers: corsHeaders });
    }

    const url = new URL(request.url);
    if (url.pathname === "/api/messages" && request.method === "POST") {
      const body = await request.json().catch(() => ({}));
      return Response.json({ ok: true, received: body }, { headers: corsHeaders });
    }

    return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
  },
};

The common Workers mistake is adding headers only to the happy path. If OPTIONS, 401, 403, or 500 responses omit CORS headers, DevTools may show only a CORS failure while hiding the real application error.

Next.js Route Handler Configuration

With the App Router, app/api/.../route.ts uses Web Request and Response objects. The Next.js docs show how to add CORS headers to a response; for credentialed APIs, use an allowlist instead of *.

// app/api/messages/route.ts
const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
]);

function getCorsHeaders(request: Request): HeadersInit | null {
  const origin = request.headers.get("Origin");
  if (!origin) return {};
  if (!allowedOrigins.has(origin)) return null;

  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Methods": "POST,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
    "Access-Control-Max-Age": "86400",
    "Vary": "Origin",
  };
}

export async function OPTIONS(request: Request) {
  const headers = getCorsHeaders(request);
  if (headers === null) {
    return Response.json({ error: "Origin not allowed" }, { status: 403 });
  }
  return new Response(null, { status: 204, headers });
}

export async function POST(request: Request) {
  const headers = getCorsHeaders(request);
  if (headers === null) {
    return Response.json({ error: "Origin not allowed" }, { status: 403 });
  }

  const body = await request.json().catch(() => ({}));
  return Response.json({ ok: true, received: body }, { headers });
}

next.config.js can attach static headers, but per-origin credentialed APIs are easier to review inside the route handler. Use config-level headers for static, public API responses only.

Test Commands

Use curl to separate preflight from the real request. Confirm that Access-Control-Allow-Origin exactly matches the request Origin, and that Access-Control-Allow-Credentials: true appears only for allowed origins.

curl -i -X OPTIONS http://localhost:8787/api/messages \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization"

curl -i -X POST http://localhost:8787/api/messages \
  -H "Origin: http://localhost:3000" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer dev-token" \
  --data '{"text":"hello"}'

curl -i -X OPTIONS http://localhost:8787/api/messages \
  -H "Origin: https://evil.example" \
  -H "Access-Control-Request-Method: POST"

Browser credential checks look like this. If credentials: "include" is used, a wildcard CORS response will be rejected by the browser.

await fetch("http://localhost:8787/api/messages", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer dev-token",
  },
  body: JSON.stringify({ text: "hello" }),
});

Practical Use Cases

The first common case is a SPA and API on separate domains. A React app on https://app.example.com and an API on https://api.example.com need an explicit allowlist. If login cookies are involved, review credentials, cookie attributes, and CSRF protection together.

The second case is an admin frontend. Add https://admin.example.com to the allowlist, but do not treat CORS as the admin permission check. Authorization still belongs in API code.

The third case is a Cloudflare Worker used as a BFF or lightweight proxy. The browser talks to the Worker, and the Worker talks to the upstream API. The Worker response still needs correct CORS headers.

The fourth case is a public read-only API. If it has no cookies, no authorization, and no private data, Access-Control-Allow-Origin: * can be acceptable. If authentication may be added later, start with an allowlist.

Specific Pitfalls

PitfallResultFix
Combining * with credentials: trueBrowser blocks the responseReturn the explicit origin
Registering https://app.example.com/Trailing slash prevents a matchStore only https://app.example.com
Allowing only localhostDifferent ports failInclude http://localhost:3000
Requiring auth for OPTIONSPreflight stops at 401/403Handle preflight before auth
Missing CORS on errorsDevTools hides the real errorAdd CORS headers to 4xx/5xx too
CDN caches origin-specific headersHeaders leak across originsAdd Vary: Origin
Treating CORS as authorizationNon-browser clients still call the APIImplement auth and CSRF separately

MDN is explicit: credentialed CORS requests cannot use Access-Control-Allow-Origin: *. If Claude Code produces that combination, treat it as a bug.

Claude Code Review Prompts

Review this repository's CORS configuration.
Check:
- No Access-Control-Allow-Origin: * when credentials are enabled
- Allowlist uses exact scheme/host/port matching
- OPTIONS preflight runs before authentication middleware
- 4xx/5xx responses include the required CORS headers
- Vary: Origin is present when responses vary by origin
If changes are needed, propose the smallest safe diff.
Diagnose this CORS error by cause.
Browser error:
<paste the full DevTools Console message>

curl preflight:
<paste curl -i -X OPTIONS output>

Expected origin:
https://app.example.com

Read the relevant API files and return reproduction steps, root cause, fix, and tests.
Review the Express/Fastify/Next.js/Workers CORS implementation as a security reviewer.
Focus on:
- Whether request origins are blindly reflected
- Whether localhost remains enabled in production
- Whether Authorization is allowed without proper authorization checks
- Whether cookie flows mention SameSite=None; Secure and CSRF protection
- Whether test commands separate preflight and the real request
Group findings as Critical, Must fix, and Improvement.

Use MDN’s CORS guide as the baseline. For implementation details, see Express cors middleware, @fastify/cors, Cloudflare Workers CORS examples, and Next.js Route Handlers. For reusable Claude Code workflows, check Claude Code commands.

Read this alongside the API development guide, web security headers guide, Cloudflare Workers guide, and code review workflow checklist.

Next Step

After replacing the sample domains with your own, run the review prompts against your repository and then use the Claude Code security best practices guide to review cookies, CSRF, authorization, and headers together. For client work or internal platform teams, this checklist can become a review artifact that supports paid implementation help or reusable template adoption.

Results From Actually Trying This

In Masa’s local test, the Express and Fastify samples ran on localhost:8787; preflight and POST succeeded from Origin: http://localhost:3000, while https://evil.example returned 403. The easiest misses were CORS headers on error responses and explicit OPTIONS handling in Workers. The most reliable workflow was to implement the allowlist first, run the curl checks, and then ask Claude Code to verify that there is no wildcard-plus-credentials combination, that Vary: Origin is present where needed, and that localhost cannot leak into production.

#Claude Code #CORS #security #API #web development
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.