You have built a clean React frontend and a robust Node.js/Express backend. You fire up both servers, attempt a simple fetch request, and your browser console turns red with the most infamous error in web development:
"Access to fetch at 'http://localhost:4000/api' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource."
This error is a rite of passage for full-stack developers. It halts progress immediately, preventing your frontend from consuming your backend API.
While it feels like a bug, this is actually a browser security feature working exactly as intended. This guide covers the root cause of Cross-Origin Resource Sharing (CORS) errors, provides the production-ready fix using Express middleware, and explains how to handle authenticated requests correctly.
Understanding the Root Cause: The Same-Origin Policy
To fix CORS, you must understand why it exists. Browsers enforce a security mechanism called the Same-Origin Policy (SOP).
By default, the SOP allows scripts running on one origin to access data only from that same origin. An "origin" is defined by the combination of three things:
- Protocol (e.g.,
http://vshttps://) - Domain (e.g.,
localhostorgoogle.com) - Port (e.g.,
:3000vs:4000)
Why Your App Fails
In a typical modern development stack:
- Your React app runs on
http://localhost:5173(Origin A). - Your Express API runs on
http://localhost:4000(Origin B).
Because the ports differ (5173 vs 4000), the browser treats them as distinct, unrelated origins. When React tries to fetch data from Express, the browser intercepts the request. It looks for specific headers from the server explicitly stating, "I allow Origin A to read my data."
If those headers are missing (which they are by default in Express), the browser blocks the response to protect the user, resulting in the error you are seeing.
The Solution: Configuring CORS in Express
The correct way to fix this is on the server. You must tell your Express application to append the correct Access-Control-Allow-Origin headers to its responses.
While you can manually set headers, the industry standard is to use the cors middleware package. It handles complex headers and preflight requests automatically.
Step 1: Install the CORS Package
In your backend (Node/Express) directory, install the package:
npm install cors
Step 2: Implement the Middleware
Open your main server entry file (usually index.js, server.js, or app.js). Import cors and initialize it before your route definitions.
Here is a modern implementation using ES Modules (Node.js 18+ syntax):
import express from 'express';
import cors from 'cors';
const app = express();
const PORT = 4000;
// OPTION 1: Allow All Origins (Default)
// Useful for public APIs or quick development.
// app.use(cors());
// OPTION 2: Restrict to Specific Domains (Production Best Practice)
const corsOptions = {
origin: 'http://localhost:5173', // Your Frontend URL
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
};
app.use(cors(corsOptions));
// JSON Body Parser (Standard Express Setup)
app.use(express.json());
// Sample Route
app.get('/api/data', (req, res) => {
res.json({ message: 'CORS is fixed!', status: 'success' });
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
How This Works
When you apply app.use(cors(corsOptions)):
- Express intercepts incoming HTTP requests.
- It compares the
Originheader sent by the browser against yourcorsOptions. - If they match, Express adds the
Access-Control-Allow-Origin: http://localhost:5173header to the response. - The browser sees this header, validates the match, and allows React to read the response body.
Handling Preflight Requests (OPTIONS)
Sometimes you might see an error related to an OPTIONS request, or you notice two requests in your network tab for a single fetch.
For "complex" requests—those using methods other than GET/POST or custom headers like Authorization—the browser performs a Preflight Request. It sends a preliminary OPTIONS request to ask the server: "Do you allow this specific method and these headers?"
If you do not use the cors middleware, your server might respond to GET but fail on OPTIONS, causing the actual request to fail. The cors middleware automatically creates a route handler for OPTIONS requests, ensuring the browser receives the necessary permissions ("Yes, PUT requests are allowed") before sending the actual data.
Advanced Scenario: Cookies and Credentials
A common pitfall occurs when adding authentication (sessions or cookies) to your app. Even with the basic CORS setup above, requests involving credentials will fail.
If your fetch request includes { credentials: 'include' }, the browser imposes stricter security rules:
- The server cannot respond with
Access-Control-Allow-Origin: *. It must specify the exact origin. - The server must send
Access-Control-Allow-Credentials: true.
Updated Server Configuration (Backend)
Modify your corsOptions to enable credentials:
const corsOptions = {
origin: 'http://localhost:5173',
credentials: true, // Crucial for cookies/sessions
};
app.use(cors(corsOptions));
Updated Fetch Request (Frontend)
In your React component, you must explicitly tell fetch to send cookies:
// React / Client-side code
const fetchData = async () => {
try {
const response = await fetch('http://localhost:4000/api/data', {
method: 'GET',
credentials: 'include', // Sends cookies with the request
});
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch error:', error);
}
};
Development Environment Alternative: Proxying
While the server-side fix is required for production, you can simplify local development by proxying requests. This tricks the browser into thinking the frontend and backend are on the same port.
If you are using Vite (the modern standard for React), configure vite.config.js:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
// Matches any request starting with /api
'/api': {
target: 'http://localhost:4000',
changeOrigin: true,
secure: false,
},
},
},
});
With this configured, your React fetch call changes from http://localhost:4000/api/data to simply /api/data. The Vite development server accepts the request and forwards it to port 4000. Since the browser sees the request going to the same origin (the frontend server), CORS logic is bypassed entirely.
Note: You still need the Express cors configuration for when you deploy your application, as this proxy only exists in your local Vite dev server.
Summary
The "Access to fetch has been blocked by CORS policy" error is a security gatekeeper, not a bug. To resolve it effectively:
- Understand: The browser blocks cross-origin resource sharing by default.
- Install: Use the
corsmiddleware in your Express backend. - Configure: Whitelist your frontend domain in the CORS options.
- Authenticate: Set
credentials: trueon the server andcredentials: 'include'on the client if using cookies.
By handling CORS properly on the backend, you ensure your application is secure and follows standard web protocols, paving the way for a smooth deployment to production.