Few errors frustrate web developers as often as this one:
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:5173andhttp://localhost:3000(different port)https://example.comandhttps://api.example.com(different host)http://example.comandhttps://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:
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-Originmust be the exact origin, never*.- Cookies also need suitable
SameSiteandSecuresettings.
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.
// 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
Originheader back without checking it against an allowlist. - Do not rely on browser extensions that disable CORS; your users will not have them.
Debugging checklist
- Open the Network tab and find the failing request. Is there an
OPTIONSrequest before it? - Check the response headers of the preflight and the real request.
- Confirm the origin, methods and headers match exactly, including the port.
- 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.