Fix CORS Errors That Work on Localhost But Break in Production (MERN)
Learn why CORS errors appear only after deploying a MERN app and how to fix them โ covering origin config, credentials, cookie flags, and preflight requests.

Fix CORS Errors That Work on Localhost But Break in Production (MERN)
A practical guide to why CORS breaks only after deployment, with exact Express.js fixes for credentials, cookies, and cross-domain preflight requests.
If your MERN app works perfectly on localhost but throws Access to fetch has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present the moment you deploy, your backend's CORS config is hardcoded for a dev environment that no longer exists in production.
Why It Works Locally But Not in Production
Locally, many devs run frontend and backend close enough (same port, proxy config, or browser extensions disabled) that CORS restrictions barely surface. In production, your frontend is usually on https://myapp.com and your API is on https://api.myapp.com or a separate host like Render/Railway entirely โ a genuine cross-origin request, so the browser enforces the full CORS handshake.
Root Causes, Ranked by Frequency
- ๐
originin your CORS config is still set tohttp://localhost:3000 - ๐ช Using
credentials: trueon the frontend butAccess-Control-Allow-Origin: *on the backend (invalid combination) - ๐ Cookies not sent because
SameSite/Secureflags aren't set for a cross-domain setup - ๐ง Preflight
OPTIONSrequests never reaching your CORS middleware (blocked by a proxy, load balancer, or route order)
Fix 1: Set the Correct Origin Per Environment
Never hardcode a single origin. Use an environment variable so dev and prod each get the correct value, and allow multiple origins if you have staging/preview deployments.
import cors from "cors";
const allowedOrigins = [
"http://localhost:3000",
"https://myapp.com",
"https://staging.myapp.com",
];
app.use(
cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
credentials: true,
})
);
Fix 2: Never Combine Wildcard Origin With Credentials
The browser will reject the response outright if Access-Control-Allow-Origin: * is paired with a credentialed request. If you need cookies or auth headers sent cross-origin, you must echo back a specific origin.
// โ Invalid with credentials
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// โ
Valid: specific origin + credentials
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true
// Frontend fetch must also opt in:
fetch("https://api.myapp.com/data", {
credentials: "include",
});
Fix 3: Cookie SameSite and Secure Flags
If your frontend and API are on different subdomains or domains, cookies set with the default SameSite=Lax will silently fail to be sent on cross-site requests.
res.cookie("token", jwtToken, {
httpOnly: true,
secure: true, // required in production (HTTPS)
sameSite: "none", // required for cross-domain requests
});
Fix 4: Make Sure CORS Middleware Runs Before Your Routes
If app.use(cors()) is placed after your route handlers, or after another middleware that terminates the request, the preflight OPTIONS request never gets the right headers.
const app = express();
// โ
CORS must be one of the very first middleware
app.use(cors({ origin: allowedOrigins, credentials: true }));
app.use(express.json());
// routes go after
app.use("/api/users", userRoutes);
Diagnostic Checklist
- โ
Open DevTools โ Network tab โ check if the
OPTIONSpreflight request even reaches your server - ๐ Confirm the response has
Access-Control-Allow-Originmatching your exact frontend URL - ๐ช If using cookies, confirm
credentials: "include"is set on every fetch/axios call - ๐ก๏ธ Check your hosting platform (Vercel/Render/Nginx) isn't stripping CORS headers via its own proxy layer
- ๐ Verify
NODE_ENVis actually set to production and loading the right env vars
Official-Style Error Explanation
No 'Access-Control-Allow-Origin' header is present on the requested resource indicates the server's response did not include the required CORS header for the requesting origin. This is a server-side configuration issue, not a frontend bug. Configure the backend to explicitly allow the production origin and retry the request.
Frequently Asked Questions
โ Can I fix CORS from the frontend only?
No. CORS headers must come from the server responding to the request. Frontend code can request credentials be included, but it cannot grant itself permission the backend hasn't allowed.
โ Is using a proxy instead of fixing CORS a good idea?
It works as a workaround (e.g. Next.js API routes or an Nginx reverse proxy making requests same-origin), but it's usually better to fix CORS properly unless you have another architectural reason for a proxy layer.
โ Why does Postman work fine but the browser fails?
Postman doesn't enforce CORS โ it's a browser security feature only. A successful Postman request only confirms your API logic works, not that your CORS headers are correct.
CORS errors in production almost always trace back to an origin, credentials, or cookie-flag mismatch between environments โ fix the config once per environment and it stays fixed.


