Skip to content
Happy Programming Guide
Start learning
Web Development

Fixing CORS Errors in React and Node

A React app on port 3000 calling an Express API on 8080 hits CORS immediately. The server-side fix, the dev-server proxy, credentials, and the production setup, with working code.

Rusty fence tied with chains and locked with a padlock

The React front end runs on localhost:3000, the Node API on localhost:8080, and the first fetch between them fails with a CORS error. Nothing is broken; the browser is refusing to hand your JavaScript a response from a different origin until the API says it is allowed. The fix lives in the Node code, takes about three lines, and this article covers it for Express, Fastify and plain Node, along with the development proxy that avoids the question entirely and the settings that production needs.

The situation#

JavaScript
// React, running at http://localhost:3000
const res = await fetch('http://localhost:8080/api/items');
// Access to fetch at 'http://localhost:8080/api/items' from origin 'http://localhost:3000'
// has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present

Different port means different origin. The request reached the API and the API answered; the browser then looked for a permission header in the response, found none, and blocked your code from reading it. React cannot add that header. The API has to.

Express#

Terminal
npm install cors
JavaScript
const express = require('express');
const cors = require('cors');

const app = express();

app.use(cors({
  origin: ['http://localhost:3000', 'https://app.example.com'],
  credentials: true,                          // only if you send cookies or auth headers
}));

app.use(express.json());
app.get('/api/items', (req, res) => res.json([{ id: 1 }]));
app.listen(8080);

The cors middleware adds the headers to every response and answers preflight OPTIONS requests automatically. Register it before your routes and before any authentication middleware, so that preflights — which carry no credentials — are not rejected with a 401 before CORS gets to answer them.

Fastify#

JavaScript
const fastify = require('fastify')();
await fastify.register(require('@fastify/cors'), {
  origin: ['http://localhost:3000'],
  credentials: true,
});

Plain Node, no framework#

JavaScript
const http = require('http');
const ALLOWED = new Set(['http://localhost:3000']);

http.createServer((req, res) => {
  const origin = req.headers.origin;
  if (ALLOWED.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Vary', 'Origin');
  }
  if (req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Max-Age', '86400');
    res.writeHead(204);
    return res.end();
  }
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify([{ id: 1 }]));
}).listen(8080);

This is what the middleware does for you. Note Vary: Origin, which stops a cache serving one origin’s permission header to another.

Preflight and middleware order#

A JSON POST, a PUT, a DELETE, or any request with an Authorization header triggers a preflight: the browser sends OPTIONS first and only sends the real request if the answer allows it. Two things break preflights:

  • Authentication middleware registered before CORS rejects the credential-less OPTIONS with 401. Move CORS above it.
  • A route handler that does not exist for OPTIONS returns 404. The middleware handles this; hand-written servers must answer OPTIONS explicitly.

In the Network tab, a failed preflight shows as a red OPTIONS request just before the request you made. That is the one to inspect.

Cookies and tokens#

Sending a session cookie or an Authorization header cross-origin needs opt-in on both sides:

JavaScript
// React
await fetch('http://localhost:8080/api/me', { credentials: 'include' });
JavaScript
// Node: credentials: true, and a specific origin - not '*'
app.use(cors({ origin: 'http://localhost:3000', credentials: true }));

Browsers reject Access-Control-Allow-Origin: * combined with credentials, which is why the origin list must be explicit. For cookies to be sent at all cross-site, they also need SameSite=None; Secure, which in turn requires HTTPS — on localhost, most browsers make an exception, but on a deployed site this is the step people miss.

The development proxy#

During development you can sidestep CORS by having the React dev server forward API calls, so the browser sees one origin:

JavaScript
// vite.config.js
export default {
  server: {
    proxy: { '/api': 'http://localhost:8080' },
  },
};
JSON
// Create React App: package.json
{ "proxy": "http://localhost:8080" }

Then fetch /api/items with a relative URL. The dev server proxies it, no CORS headers needed. This is convenient and it hides the problem: the moment you deploy the front end and API to different origins, CORS is back. Set up the server-side headers anyway, so production is configured before you get there.

Production#

  • Put the real front-end origin in the allowed list from an environment variable, not a hard-coded localhost.
  • Or avoid cross-origin entirely: serve the API under the same domain as the front end, with a reverse proxy routing /api to Node. Same origin, no CORS, and cookies are simpler.
  • Check that error responses carry the CORS headers too. An unhandled exception that bypasses the middleware produces a 500 the browser reports as a CORS error, hiding the real cause.
JavaScript
app.use(cors({ origin: process.env.FRONTEND_ORIGIN.split(','), credentials: true }));

Questions people ask#

Why does it work in Postman?

Postman is not a browser. CORS is enforced only by browsers, on behalf of the user’s cookies.

Do I need CORS if React and Node are on the same server?

Only if they are on different origins. Same domain, scheme and port means no CORS at all, which is a good reason to deploy them that way.

Next.js API routes?

Same-origin by default, so no CORS between a Next.js page and its own API routes. Calling an external API still needs that API’s permission, or a route of yours that proxies it.

The headers are set and it still fails.

Check the preflight in the Network tab, check the origin matches exactly including scheme and port, and check the failing response is not a 500 that skipped the middleware.

Where to go next#

Handling fetch errors properly in JavaScriptRead next

Keep reading

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *