Skip to main content
Published
CORSSecuritySPA

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.

lib/cors.tsts
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 });
}
Allowlist origins. Reflect only known values. Never combine * with credentials.
app/api/items/route.tsts
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 });
}
Apply CORS on both preflight and the real response.

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.

More writing

Availability

Open to opportunities

Open to senior product engineer roles with real ownership across the stack. Security-minded by default.

UK-based · Remote / hybrid · Permanent or contract