Fix CORS Errors in React + Node.js Apps

September 22, 2026 · 8 views
Fix CORS Errors in React + Node.js Apps

You send a request from your React app running on localhost:3000 to your Node.js API on localhost:5000, and the browser console throws: Access to fetch at 'http://localhost:5000/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. The request works fine in Postman, the API logs show it never even hit your route handler in some cases, and nothing you change on the frontend fixes it — because the fix for a CORS error in a React and Node.js app almost never belongs on the frontend at all.

This is one of the most searched errors in full-stack JavaScript development, and it trips up developers at every level because the error message names the symptom (a missing header) but not the cause (a backend that never sent it). This guide walks through why the error happens, how the browser's CORS mechanism actually works, and the exact Express configuration that resolves it — plus the mistakes that make people think they've fixed it when they haven't.

What CORS actually is (and why the browser is the one blocking you)

CORS — Cross-Origin Resource Sharing — is a browser security mechanism, not a server-side restriction and not a network firewall. When your React app on http://localhost:3000 calls an API on http://localhost:5000, those are two different origins (different port, in this case — origin is defined by scheme + host + port). By default, browsers block a script from reading the response of a cross-origin request unless the server explicitly says it's allowed.

The critical detail: your Node.js server almost certainly processes the request fine. The response comes back from the server, but the browser refuses to hand that response to your JavaScript code because the response is missing an Access-Control-Allow-Origin header that names your frontend's origin as permitted. That's why the error shows up in the browser console and not as a failed request in your server logs — the server did its job; the browser is the one enforcing the block.

Simple requests vs. preflight requests

Not every cross-origin request behaves the same way, which is why some endpoints seem to work and others don't:

  • Simple requests — plain GET or POST requests with standard headers and content types like text/plain — go straight to the server, and the browser only checks the response headers afterward.
  • Preflighted requests — anything using PUT, PATCH, DELETE, a custom header like Authorization, or a JSON content type — trigger an OPTIONS request first. The browser asks the server "will you accept this?" before sending the real request, and if your server doesn't respond correctly to that OPTIONS call, the real request never even fires.

This is why a GET request can work while a POST with a JSON body throws a CORS error on the exact same API — the POST is triggering a preflight your server isn't configured to handle.

Step-by-step: fixing CORS in an Express + React app

The fix lives entirely on the Node.js side. The cleanest, most maintainable approach is the official cors middleware rather than hand-writing headers:

// server.js
const express = require('express');
const cors = require('cors');
const app = express();

const allowedOrigins = [
  'http://localhost:3000',
  'https://your-production-frontend.com',
];

app.use(cors({
  origin: (origin, callback) => {
    // allow tools like curl/Postman with no origin
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true, // only needed if you send cookies/auth headers
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

app.use(express.json());
// your routes go here, after the CORS middleware

Three details make or break this:

  1. app.use(cors(...)) must be registered before your route handlers — if a route is defined first, requests to it never pass through the CORS middleware.
  2. If your frontend sends cookies or an Authorization header with credentials, credentials: true on the server must be paired with credentials: 'include' in your fetch/axios call on the frontend — and when credentials: true is set, origin cannot be the wildcard '*', it must be a specific origin or function like above.
  3. Express automatically handles the OPTIONS preflight once the cors middleware is registered — you don't need a separate app.options() route unless you're using a router setup that intercepts it first.

Common mistakes that keep the error alive

  • Setting Access-Control-Allow-Origin: * and also credentials: true. Browsers reject this combination outright — a wildcard origin cannot be paired with credentialed requests, and the error you'll see is a different but related CORS message.
  • Registering CORS middleware after your routes, or only on some routes and not others, so some endpoints work and others mysteriously don't.
  • Restarting the frontend but not the backend after editing the CORS config — Node.js won't pick up the change without a restart (or without nodemon watching the file).
  • Trying to "fix" CORS by adding headers on the frontend request — headers like Access-Control-Allow-Origin only mean anything when sent by the server in the response; adding them to your fetch request does nothing.
  • Confusing CORS with a network/connectivity issue. If the request never appears in your Network tab as a real call at all, the problem is usually the URL, a dead server, or a mixed-content HTTP/HTTPS mismatch — not CORS.

Best practices for production

  • Never leave origin: '*' in a production API that also handles authenticated requests — always whitelist specific domains.
  • Keep your allowed-origins list in an environment variable so staging and production don't share a hardcoded array.
  • If you're behind a reverse proxy (Nginx, a load balancer), verify it isn't stripping or duplicating CORS headers — a duplicated Access-Control-Allow-Origin header is itself a valid cause of the browser rejecting the response.
  • Log rejected origins in your CORS origin callback during rollout so you can catch a missed subdomain before it becomes a support ticket.

Frequently Asked Questions

Why does my request work in Postman but fail in the browser? Postman doesn't enforce CORS — it's not a browser, so it has no same-origin policy to enforce. A request succeeding in Postman or curl while failing in the browser is actually confirmation that the issue is CORS, not server logic or authentication.

Does a CORS error mean my API is broken? No. In most cases the API executed correctly and returned a valid response — the browser is discarding that response before your JavaScript can read it, because the required header wasn't present.

Can I fix CORS entirely from the React frontend? Not in production. During local development you can use a dev-server proxy (e.g. the proxy field in Create React App, or Vite's server.proxy) to route API calls through the same origin, which sidesteps CORS locally — but the real backend still needs correct CORS headers for any environment where the frontend and API are genuinely served from different origins.

Why does adding mode: 'no-cors' to my fetch call not fix it? no-cors mode makes the browser send the request but return an opaque response your JavaScript can't read at all — no status, no body, no headers. It stops the console error from appearing, but your app also can't use the response, so it isn't a fix, just a different failure mode.

Key Takeaways

A CORS error in a React and Node.js app is a browser enforcing a missing Access-Control-Allow-Origin header — it is fixed on the server, not the client, by correctly configuring the cors middleware before your routes, matching credentials settings on both ends, and whitelisting real origins instead of relying on a wildcard once credentials are involved. Get the Express configuration right once, keep the allowed-origins list environment-specific, and this error stops being a recurring debugging session and becomes a five-minute check.

#nodejs #express #api-security #react #cors
Share this article:

0 Comments

No comments yet — be the first to share your thoughts.

Leave a comment

Never published.