How to Fix a CORS Error in React and Node.js
Fix a CORS error in React and Node.js: Express cors config, preflight OPTIONS, credentials, and a Vite proxy when you don't control the API.
- Author
- Swagat Murmu
- Published
- Reading time
- 9 min
- Category
- Web Development
How to Fix a CORS Error in React and Node.js#
Your React app is on http://localhost:5173. The Express API is on port 5000. The route works, then the console says the request has been blocked by CORS policy. Nothing is wrong with fetch. The browser refused to give the response to JavaScript because Node.js never named that origin as allowed.
Fix it on the server you control. If you don't control the server, proxy the call through your own backend. Don't "fix" it by turning Chrome security off.
How do you fix a CORS error in React and Node.js?#
Install the cors package on Express and allow your real frontend origin. That is the usual Node Express CORS error fix.
npm install cors
Add this configuration to your backend entry file, such as server.js:
const express = require("express");
const cors = require("cors");
const app = express();
app.use(cors({
origin: "http://localhost:5173"
}));
app.use(express.json());
app.get("/api/hello", (req, res) => {
res.json({ message: "API connected successfully!" });
});
app.listen(5000, () => {
console.log("Server running on port 5000");
});
Restart Node. Try the request again.
http://localhost:5173 is the default Vite URL. Create React App usually uses port 3000. Copy the origin from the address bar: scheme, host, and port. A wrong port is a different origin. http://localhost:5173 and http://127.0.0.1:5173 are different too.
You solve a CORS error in React and Node.js on the process that sends the response. A change inside a React component will not add Access-Control-Allow-Origin.
Why has the request been blocked by CORS policy in Node.js?#
The browser blocked it because the Express response had no Access-Control-Allow-Origin for your page. React does not enforce CORS. The browser does.
An origin is scheme + host + port.
- Frontend:
http://localhost:5173 - Backend:
http://localhost:5000
Same laptop, different ports, different origins. When the page calls the API, Chrome checks the response headers. If they don't permit this page, you get some version of:
Access to fetch at 'http://localhost:5000/api/hello' from origin 'http://localhost:5173' has been blocked by CORS policy
That's the React–Express CORS policy blocked message, and it's the normal React localhost CORS error with Express. It is not a 401. If the Network row fails before you can read the JSON, look at headers first, not at your login code.
Postman will still return the JSON. Postman is not a browser.
Why is the Express CORS middleware not working?#
cors() is probably registered too late, or the server was never restarted. Middleware does not run for routes mounted above it.
const cors = require("cors");
// This must come before routes and before express.Router() mounts.
app.use(cors({
origin: "http://localhost:5173"
}));
app.use("/api", apiRouter);
A safe npm cors Express configuration is an explicit origin, registered once, near the top of server.js. app.use(cors()) with no options reflects whatever origin called you. Fine on a classroom demo. Loose once the API is on the public internet.
If it still fails, check these in order:
- The frontend is calling the port Express actually listens on.
- You saved the file the running process loaded. Nodemon users forget a second entry file all the time.
- The allowed origin matches the page exactly. No mixed
localhostvs127.0.0.1. - The failure is an OPTIONS preflight, not the GET you think you're debugging.
How do you allow multiple origins in Express CORS?#
Pass an array of exact origins. Express echoes the one that matches the request.
app.use(cors({
origin: [
"http://localhost:5173",
"http://localhost:3000",
"https://app.example.com"
]
}));
Use that when you switch between Vite and Create React App, or between your laptop and a staging URL. Don't put "*" in that list once cookies are involved. Browsers reject a wildcard Access-Control-Allow-Origin on credentialed requests.
Preview deploys and tenant subdomains won't fit a hard-coded list. Use a function and fail closed:
const allowed = new Set([
"http://localhost:5173",
"http://localhost:3000"
]);
app.use(cors({
origin(origin, callback) {
// curl and server-to-server calls often send no Origin
if (!origin || allowed.has(origin)) {
callback(null, true);
return;
}
callback(new Error("Origin not allowed by CORS"));
}
}));
callback(null, true) reflects the request origin. That's what you want with credentials. Reflecting every Origin header with no allow-list is the same as leaving the door open.
How do you set Access-Control-Allow-Origin in React and Node?#
Set it on the Node.js response. There is nowhere useful to set Access-Control-Allow-Origin inside React. The browser only trusts the header that arrived from the other origin.
You can skip the package and write the headers yourself:
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
res.setHeader(
"Access-Control-Allow-Methods",
"GET,POST,PUT,PATCH,DELETE,OPTIONS"
);
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization"
);
if (req.method === "OPTIONS") {
res.sendStatus(204);
return;
}
next();
});
Miss the OPTIONS branch and simple GETs may work while every JSON POST dies. The cors package does this bookkeeping for you. Hand-rolled headers are fine if you actually send all three, on the preflight as well as the real response.
Why does a POST throw a CORS error when GET works?#
A CORS error on a POST request in Express is almost always a failed preflight. The browser sends OPTIONS before the POST. That CORS preflight request asks whether POST and Content-Type: application/json are allowed. If OPTIONS 404s, hits your auth wall, or comes back without the right headers, the POST never leaves the browser.
JSON is not a "simple" content type. A GET with no custom headers often skips preflight. A POST from Axios or fetch with a JSON body does not.
app.use(cors({
origin: "http://localhost:5173",
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"]
}));
Watch the Network panel. You'll see OPTIONS sitting above the failing POST. Preflight does not include the Authorization header, so don't run JWT middleware before CORS. The preflight has no token. It will never have one.
What does Express CORS credentials true actually change?#
It lets the browser expose the response to JavaScript when the request carried cookies. You have to opt in on both sides. The origin must be specific, not *.
app.use(cors({
origin: "http://localhost:5173",
credentials: true
}));
fetch("http://localhost:5000/api/me", {
credentials: "include"
});
Axios uses a different flag for the same thing:
axios.get("http://localhost:5000/api/me", {
withCredentials: true
});
Leave either side off and the cookie stays put. SameSite=Strict also blocks cookies on a cross-site XHR. Across two localhost ports, that fight gets ugly fast, because SameSite=None wants Secure, and plain http://localhost isn't a real HTTPS setup in every browser version you'll test. A dev proxy avoids the cookie problem by making the call same-origin.
How do you fix an Axios CORS error in React and Node.js?#
Fix the Express headers. Axios cannot opt out of the browser. An Axios CORS error in React and Node.js is still a missing or mismatched CORS header, usually on OPTIONS, because Axios defaults POST bodies to Content-Type: application/json.
import axios from "axios";
const api = axios.create({
baseURL: "http://localhost:5000"
});
api.post("/api/login", { email, password })
.then((res) => console.log(res.data))
.catch((err) => {
// CORS failures show up as "Network Error" with no status.
console.error(err.message, err.response?.status);
});
A baseURL does not change the origin check. If withCredentials: true is set, the server needs credentials: true and an explicit origin. Open DevTools when Axios says "Network Error" and the status is blank. The browser hid the body. The CORS error is in the console, not in err.response.
How do you set up a Vite React CORS proxy?#
Proxy /api through the Vite dev server so the browser only talks to one origin. This Vite React CORS proxy setup is a development convenience, not a production config.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": {
target: "http://localhost:5000",
changeOrigin: true
}
}
}
});
Then call a relative URL:
fetch("/api/hello");
Restart Vite after editing vite.config.js. The browser requests http://localhost:5173/api/hello. Vite forwards that to port 5000. Same origin, so CORS never enters the picture.
A static host will not run this proxy. Netlify, S3, and a plain Vercel static deploy serve files. They do not exec your Vite config. In production, put Nginx or the platform rewrite in front of both apps, or send real CORS headers from Express.
When should you use a React CORS proxy backend?#
When the third-party API won't allow your origin, or the call needs a secret. A React CORS proxy backend is an Express route that calls the other API and returns the data to React.
Two options, and only two:
- Ask the provider to allow your origin, if they even support browser access.
- Call them from Node.js. The browser only talks to you.
app.get("/api/weather", async (req, res) => {
const city = String(req.query.city || "").slice(0, 80);
const upstream = await fetch(
`https://api.example.com/weather?q=${encodeURIComponent(city)}`,
{
headers: { Authorization: `Bearer ${process.env.WEATHER_TOKEN}` }
}
);
const data = await upstream.json();
res.status(upstream.status).json(data);
});
Keep the token in a server env var. A VITE_ variable is compiled into the client bundle. Anyone can read it.
Don't accept an arbitrary URL from the query string and fetch it. That's an open proxy. Pin the host, cap the input, and don't forward the user's cookies upstream.
Why does the API work in Postman and fail in React?#
Postman never applies CORS. A 200 there means the route exists and returns a body. It says nothing about Access-Control-Allow-Origin.
Check the call the browser actually made:
- DevTools → Network. Trigger it from the React page, not from Postman.
- Open the red request. If an OPTIONS row sits above it, the preflight failed.
- Read the response headers.
Access-Control-Allow-Originmust be your page's origin, or the header must be missing and you're done looking. - Match scheme, host, and port.
https://localhost:5173is nothttp://localhost:5173.
A request that is red with status (failed) and an empty response is the browser dropping it. Your route handler may have run anyway. CORS runs in the browser after the response arrives.
What should you check before changing React?#
Confirm Express allows this exact origin, then restart Node. Work top to bottom. Stop at the first miss.
- The API URL is right, and something is listening.
curl http://localhost:5000/api/helloshould print JSON. corsis installed, andapp.use(cors(...))sits above every route.originmatches the address bar, port included.- The Node process was restarted after the edit.
- POST or PUT with JSON: OPTIONS returns 204 (or 200) with
Access-Control-Allow-MethodsandAccess-Control-Allow-Headers. - Cookies:
credentials: trueon the server,credentials: "include"or AxioswithCredentials: trueon the client, no wildcard origin. - Someone else's API: stop sending them CORS headers they will never read. Proxy it, or use the origin they already allow.
A browser extension that disables CORS only fixes your laptop. The next person who opens the app still gets blocked.
Does CORS secure the API?#
No. CORS tells a browser which sites may read a response from JavaScript. It does not check who the caller is.
curl, a mobile app, and another server ignore it. Put auth on the route, validate the body, and rate-limit anything that costs money. Treat the cors middleware as browser plumbing, not as a lock.
Further reading:
Share this article
Found this useful? Share it with others.