You asked AI to scaffold a backend, pointed your frontend at it, and instantly hit a wall of red CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. errors. So you did what everyone does, you asked AI to fix it. The AI slapped Access-Control-Allow-Origin: * on every response and the errors vanished. Shipped it. Three weeks later, your authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. cookies stopped working in production. Understanding what CORS actually does, and why AI's default fix is almost always the wrong one, turns a recurring headache into a five-minute configuration.
What an originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443. is
Browsers define an "origin" as the combination of three things: the protocolWhat is protocol?An agreed-upon set of rules for how two systems communicate, defining the format of messages and the expected sequence of exchanges. (http vs https), the hostname, and the port. All three must match for two URLs to share the same origin.
https://example.com:443/page-a
https://example.com:443/page-b <- same origin (path doesn't matter)
http://localhost:3000 <- different protocol AND port from the next one
https://localhost:3000 <- different protocol
https://example.com
https://api.example.com <- different subdomain = different origin
https://example.com
https://example.com:8080 <- different port = different originThe same-origin policyWhat is same-origin policy?A browser security rule that prevents JavaScript on one origin from reading responses from a different origin. is baked into every browser. When your JavaScript at http://localhost:3000 tries to fetch from http://localhost:4000, the browser sees two different origins and steps in.
Why CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. exists
Imagine you're logged into your bank. The bank's server trusts requests from your browser because your sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. cookieWhat is cookie?A small piece of data the browser stores and automatically sends with every request to the matching server, often used for sessions. is automatically attached to every request. Now imagine you visit a malicious site in another tab. Without any cross-originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443. restrictions, that site's JavaScript could silently do this:
fetch('https://yourbank.com/transfer', {
method: 'POST',
body: JSON.stringify({ to: 'attacker', amount: 5000 }),
credentials: 'include' // your session cookie goes along for the ride
});The bank sees a valid session cookie and processes the transfer. You never clicked anything. This attack is called Cross-Site Request ForgeryWhat is csrf?Cross-Site Request Forgery - an attack where a malicious website tricks your browser into sending a request to another site where you're logged in. (CSRF), and the same-origin policyWhat is same-origin policy?A browser security rule that prevents JavaScript on one origin from reading responses from a different origin. is one of the main defenses against it. CORS is the mechanism that lets servers selectively relax that policy for trusted origins.
How the browser checks cross-originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443. requests
Simple requests
For "simple" requests (GET or POST with only basic headers and form-like content types), the browser sends the real request immediately but attaches an Origin header:
GET /api/users HTTP/1.1
Host: api.example.com
Origin: http://localhost:3000The server must respond with the right header or the browser will block JavaScript from reading the response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000If the header is missing or the value doesn't match, the browser suppresses the response from your JavaScript. The request did reach the server, the server just didn't give permission to read the result.
Preflight requests
For anything more complex, PUT, DELETE, custom headers like Authorization, JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. content type, the browser first sends a preflight OPTIONS request to ask the server what it allows:
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, AuthorizationThe server must respond with explicit permissions:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400Only if that preflight succeeds will the browser send the actual request. Access-Control-Max-Age caches the preflight result so the browser doesn't repeat it for every request, 86400 seconds is one day.
What AI generates vs what you need
When AI generates CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. configuration, it almost always takes the path of least resistance. Here is what that looks like compared to what production actually requires:
| What AI generates | What production needs | Why it matters |
|---|---|---|
origin: '*' | Explicit origin allowlist | Wildcard breaks credentials and opens you to CSRF |
No credentials option | credentials: true with explicit origin | Auth cookies won't cross origins without both |
| Single origin string | Dynamic origin function | Staging, production, and local dev need different origins |
No Access-Control-Max-Age | maxAge: 86400 | Without caching, browsers preflight every single request |
| CORS middleware after routes | CORS middleware before all routes | Order matters, routes hit before CORS middleware get no headers |
origin: '*' almost every time. This silences the error immediately but breaks the moment you add credentials: 'include' for authentication cookies. Browsers refuse the wildcard-plus-credentials combination as a security rule. Always specify your actual frontend origin.Configuring CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. in Express
The easiest production-ready approach is the cors npm package with a dynamic originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443. function:
const cors = require('cors');
const allowedOrigins = [
'https://myapp.com',
'https://staging.myapp.com',
'http://localhost:3000'
];
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, server-to-server)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true // allows cookies to cross origins
}));When you set credentials: true, you must also specify an explicit origin, you cannot use origin: '*' with credentials. Browsers will reject it as a security violation.
origin: true (which reflects the requesting origin back), it effectively behaves like a wildcard, any origin is accepted. This is convenient for development but dangerous in production because it defeats the purpose of CORS entirely. Use an explicit allowlist instead.Debugging CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. errors
When you see a CORS error in the browser console, the first step is to look at the Network tab in DevTools. Find the failing request and check its Response Headers. You'll usually see one of these situations:
| Symptom | Likely cause | Fix |
|---|---|---|
No Access-Control-Allow-Origin header at all | CORS middleware not running | Add cors() before your routes |
| Header present but wrong value | Origin mismatch in allowlist | Add the requesting origin to your list |
| Preflight returns 404 or 405 | OPTIONS not handled | Ensure CORS middleware handles OPTIONS |
| Works without credentials but not with | Wildcard + credentials conflict | Use explicit origin, not * |
Development workarounds
During development, routing your frontend's APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. calls through the same dev server sidesteps CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. entirely because both sides share an originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443.:
// In Vite config - proxy /api to your backend
server: {
proxy: {
'/api': 'http://localhost:4000'
}
}Your frontend fetches /api/users, the dev server forwards it to http://localhost:4000/api/users, and the browser never sees a cross-origin request. This is safe for development but don't rely on it in production, configure proper CORS headers on your actual server.
Quick reference
| Header | Purpose | Example value |
|---|---|---|
Access-Control-Allow-Origin | Which origins can read the response | https://myapp.com |
Access-Control-Allow-Methods | Which HTTP methods are permitted | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | Which request headers are allowed | Content-Type, Authorization |
Access-Control-Allow-Credentials | Whether cookies may cross origins | true |
Access-Control-Max-Age | Cache preflight response (seconds) | 86400 |
// Production CORS setup, what AI should have generated
const express = require('express');
const cors = require('cors');
const app = express();
const corsOptions = {
origin: function (origin, callback) {
if (!origin) return callback(null, true);
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
'https://staging.myapp.com'
];
if (process.env.NODE_ENV === 'development') {
allowedOrigins.push('http://localhost:3000');
}
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
optionsSuccessStatus: 200
};
app.use(cors(corsOptions));
app.get('/api/users', (req, res) => {
res.json({ users: ['Alice', 'Bob'] });
});
app.listen(4000);