Few errors frustrate web developers as often as this one:

text
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:5173'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

The key to fixing it is understanding who is blocking what. The browser blocks your frontend from reading a response from a different origin unless the server explicitly allows it. The fix is almost always on the server.

What counts as a different origin

An origin is the combination of scheme, host and port. These are all different origins:

  • http://localhost:5173 and http://localhost:3000 (different port)
  • https://example.com and https://api.example.com (different host)
  • http://example.com and https://example.com (different scheme)

Simple requests vs preflight

For "simple" requests (GET or POST with basic headers), the browser sends the request and checks the response headers.

For anything else, such as PUT, DELETE, JSON bodies with Content-Type: application/json or custom headers like Authorization, the browser first sends an OPTIONS preflight request. If the preflight response does not allow the method and headers, the real request never happens.

The fix: configure the server

Allow your frontend's origin explicitly. In Express:

javascript
import cors from "cors";

app.use(cors({
  origin: ["https://app.example.com", "http://localhost:5173"],
  methods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
  credentials: true,
}));

The server must answer the preflight OPTIONS request too. Many frameworks do this automatically once CORS is configured; with a custom setup, make sure OPTIONS routes are not blocked by authentication middleware.

Cookies and credentials

If you send cookies (credentials: "include"):

  • The server must return Access-Control-Allow-Credentials: true.
  • Access-Control-Allow-Origin must be the exact origin, never *.
  • Cookies also need suitable SameSite and Secure settings.

During development: use a proxy

Instead of opening CORS for localhost on your API, proxy requests through your dev server so the browser sees one origin.

javascript
// vite.config.js
export default {
  server: { proxy: { "/api": "https://api.example.com" } },
};

What not to do

  • Do not set Access-Control-Allow-Origin: * on APIs that use cookies or return private data.
  • Do not reflect any incoming Origin header back without checking it against an allowlist.
  • Do not rely on browser extensions that disable CORS; your users will not have them.

Debugging checklist

  1. Open the Network tab and find the failing request. Is there an OPTIONS request before it?
  2. Check the response headers of the preflight and the real request.
  3. Confirm the origin, methods and headers match exactly, including the port.
  4. Check that errors (like 401 or 500) also include CORS headers, or the browser hides the real error.

Key takeaways

  • CORS is enforced by the browser and fixed on the server.
  • Non-simple requests trigger an OPTIONS preflight that must succeed.
  • With credentials, use an exact origin and Allow-Credentials: true.
  • Use a dev proxy locally and a strict allowlist in production.