CORS for frontend engineers
What CORS actually controls in the browser, why wildcard origins with credentials are dangerous, and why same-origin BFFs stay simpler.
Cross-origin resource sharing is a browser rule set, not an authentication system. CORS tells the browser when a frontend on origin A may read a response from origin B.
Frontend engineers meet CORS when a SPA on localhost or a marketing domain calls an API elsewhere. The dangerous fix is Access-Control-Allow-Origin: * with credentials. That is how you publish private data to the web.
What the browser actually checks
Simple requests and preflights both matter. Custom headers, JSON content types, and credentialed calls trigger OPTIONS. Your API must answer the preflight correctly or the real call never happens.
const ALLOWED = new Set([
"https://app.example.com",
"http://localhost:3000",
]);
export function corsHeaders(req: Request): Headers | null {
const origin = req.headers.get("Origin");
if (!origin || !ALLOWED.has(origin)) return null;
const headers = new Headers();
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Vary", "Origin");
headers.set("Access-Control-Allow-Credentials", "true");
headers.set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
);
headers.set(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-CSRF-Token"
);
headers.set("Access-Control-Max-Age", "86400");
return headers;
}
export function handlePreflight(req: Request) {
if (req.method !== "OPTIONS") return null;
const headers = corsHeaders(req);
if (!headers) return new Response(null, { status: 403 });
return new Response(null, { status: 204, headers });
}
import { corsHeaders, handlePreflight } from "@/lib/cors";
export async function OPTIONS(req: Request) {
return handlePreflight(req) ?? new Response(null, { status: 403 });
}
export async function GET(req: Request) {
const headers = corsHeaders(req) ?? new Headers();
const data = { items: [] };
return Response.json(data, { headers });
}
Frontend guidance
Prefer same-origin via Next.js rewrites or a BFF. No CORS theatre for first-party product calls.
If you must go cross-origin, list exact origins. Never pair wildcard origins with cookies. Do not treat CORS errors as proof the API is secure. CORS does not stop Postman, curl, or a hostile native app.
- Same-origin first.
- Explicit allowlist second.
- Credentials only with a concrete origin, never *.
- Answer OPTIONS with the same allowlist rules as the real route.