Next.js

Create React App's CORS Header Goes on the API, Not the App

Which of this 2017 question's three answers still applies today, the one react-scripts already handles for you by default, and the fix that works for the rest.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

Create React App's dev server cannot fix a CORS error for a different server. The Access-Control-Allow-Origin header has to come from whichever server actually answers the request — for most projects, that is the backend API, not the dev server running your React code. For local development, switch the call to a relative path and add CRA's own proxy field to package.json: the browser then never leaves the dev server's own origin, so no CORS check applies at all. That convenience stops at npm start — in production, either put a reverse proxy in front of the built site so the same relative path still resolves, or point the app at the API's real URL and send CORS headers from the API itself.

The error#

This is a real Stack Overflow question from 2017, with no accepted answer: "I'm trying to figure out where to add the 'Access-Control-Allow-Origin' header to my create-react-app dev Server config." The three answers it collected disagree, which is itself a clue that the question has more than one right context.

Create React App's own documentation shows the browser-side symptom that sends people looking for this fix — a call from the page at localhost:3000 to a backend on another port. This is quoted directly from CRA's "Proxying API Requests in Development" page, not reproduced here:

text
Fetch API cannot load http://localhost:4000/api/todos. No 'Access-Control-Allow-Origin'
header is present on the requested resource. Origin 'http://localhost:3000' is therefore
not allowed access. If an opaque response serves your needs, set the request's mode to
'no-cors' to fetch the resource with CORS disabled.

Taking the browser's "no-cors" suggestion and switching the request's mode is a trap covered separately — it hides the error instead of fixing it, and silently drops any Authorization header.

Why it happens#

CORS is enforced by the browser, not by a server or by a tool like curl or Postman. MDN's CORS guide puts it plainly: browsers restrict cross-origin requests from scripts unless the response carries the right header. The cors npm package's own README states the same mechanism from the server side: "This package sets response headers—it doesn't block requests. CORS is enforced by browsers: they check the headers and decide if JavaScript can read the response. Non-browser clients (curl, Postman, other servers) ignore CORS entirely."

That is the piece the original question is missing. A create-react-app dev server has exactly one job relevant here: serving the React app itself at localhost:3000. If the app's own code then calls fetch('http://localhost:5000/api/data'), that second request goes straight from the browser to port 5000 — the dev server on port 3000 is not involved in it at all, and nothing you configure on port 3000 can add a header to a response that port 5000 sends. The header belongs on whichever process actually answers GET /api/data.

A short reproduction confirms the part that is easy to get backwards (node:20-alpine, Node 20.20.2, express 4.21.2, cors 2.8.5, 26 October 2026): a plain Express server with no CORS middleware answers normally, with no Access-Control-Allow-Origin header, and the request still succeeds from Node's own fetch — because fetch, like curl, does not enforce CORS either. Adding the cors middleware to the same route adds Access-Control-Allow-Origin: * to the response. A browser page is the only thing that would have treated the first response differently: for a simple request it would have received the same 200 response over the wire, then refused to hand it to the page's JavaScript; for a preflighted one (a Content-Type such as application/json, or a custom header), it would have stopped at the OPTIONS preflight instead and never sent the real request at all.

Fix#

1. Your React code calls a separate backend (the common case). This is the question's third answer, and the fix CRA itself documents. First, change the call itself: fetch('http://localhost:5000/api/data'), the example above, has to become a relative fetch('/api/data') — CRA's proxy only steps in for requests that stay on the page's own origin. Then add CRA's own proxy field to package.json — no extra package to install or version to manage:

json
{
  "proxy": "http://localhost:5000"
}

Create React App's documentation describes exactly this effect: "when you fetch('/api/todos') in development, the development server will recognize that it's not a static asset, and will proxy your request to http://localhost:4000/api/todos as a fallback," which "conveniently... avoids CORS issues" (from the same proxying guide). The browser only ever talks to the dev server's own origin; the dev server forwards the request to the backend itself, server to server, where CORS does not apply. The same page is explicit that this "only has effect in development (with npm start)" — it is not a production fix. It also turns on a stricter host check: if npm start then crashes with Invalid options object... allowedHosts[0] should be a non-empty string, which happens when HOST names a specific host or the machine has no private IPv4 address, that's a separate, already-documented crash with its own fix.

2. You need more than one target, or a path rewrite. CRA supports a src/setupProxy.js file for this, with no need to eject, since react-scripts@2.0.0 (documented on the same page). Install http-proxy-middleware pinned to major version 2 — the line webpack-dev-server itself depends on (webpack-dev-server@4.15.2's own dependencies field lists http-proxy-middleware: ^2.0.3, and react-scripts 5.0.1 depends on webpack-dev-server ^4.6.0):

bash
npm install http-proxy-middleware@2 --save
js
// src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');
 
module.exports = function (app) {
  app.use('/api', createProxyMiddleware({ target: 'http://localhost:5000', changeOrigin: true }));
};

The version matters. Mounted on a plain Express app — the same shape react-scripts hands to setupProxy.js — this exact sample, reproduced on 26 October 2026, forwards correctly on http-proxy-middleware@2.0.10 (the backend sees receivedUrl: '/api/data'), but 404s on both 3.0.10 (on Node 20.20.2) and 4.2.1 (confirmed on Node 24.21.0, inside that version's own supported range) with the backend receiving /data instead. Both newer majors match http-proxy-middleware's own migration guide: from version 3 onward, "when proxy is mounted on a path, this path should be provided in the target" — the mount prefix (/api) is no longer added back by default. An unpinned npm install http-proxy-middleware --save lands on one or the other depending on the Node version running it — 3.0.10 on Node 20.20.2, 4.2.1 on Node 24.21.0, both reproduced the same day — because the registry's latest, 4.2.1, declares engines that exclude Node 20.

3. The fix that still works after you deploy. The proxy field and setupProxy.js are both development-only — CRA's own docs say plainly that "it is up to you to ensure that URLs like /api/todos point to the right thing in production," and list "Enable CORS on your server" and "Use environment variables to inject the right server host and port into your app" as two of its alternatives (same proxying guide). In practice that is one of two shapes:

When the dev server itself is the actual target#

One of the original question's answers describes a different situation entirely: a page on another origin (in that case, a Salesforce Visualforce page tunnelled through ngrok) loading static assets directly from the CRA dev server. There, the dev server genuinely is the server answering the request, so its own response headers are what matters — that answer reports editing webpackDevServer.config.js directly to add them. The question's top-voted answer shows the same kind of change made through a third-party tool instead, react-app-rewired's config-overrides.js, which hands back the dev server config for editing without ejecting.

Today that workaround is largely unnecessary for a current project. Reading react-scripts' own dev server configuration shows it now sets Access-Control-Allow-Origin: '*' by default: absent in react-scripts 4.0.3, present from react-scripts 5.0.0 onward, unchanged in 5.0.1 — the latest react-scripts on npm. The header those two answers added by hand is already in the config react-scripts 5 ships, so neither ejecting nor a config override is needed for this case any more.

Check the console message from your CRA app#

Paste the exact console message below to see which CORS case applies and the server-side fix for your stack. It runs in your browser; nothing is sent.

Everything runs in your browser — nothing you paste is sent anywhere, and tokens, cookies and passwords are masked in the output.

Optional: response headers (curl -i / curl -v output)
Try:

Verify the fix#

With the proxy field set and the backend running, request an API path with a non-HTML Accept header — CRA's dev server only forwards requests that don't ask for text/html:

bash
curl -s -H "Accept: application/json" http://localhost:3000/api/data

The backend's own JSON should come back, from port 3000, with no CORS error possible, because the browser never made a cross-origin request in the first place. If you added CORS headers on the API instead, open the Network tab, repeat the call directly against the API's own port, and confirm the response carries Access-Control-Allow-Origin for your page's origin.

The latest react-scripts on npm, the package behind the proxy field and setupProxy.js above, is still 5.0.1, published April 2022, and the create-react-app repository's README now carries a caution banner: "Create React App... is now in long-term stasis and we recommend that you migrate to one of React frameworks documented on Start a New React Project". Whichever fix above gets a current project working again, it is worth knowing that Vite migrations bring their own errors — local packages losing their exports is one of them — rather than assuming the move itself is free.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote