Skip to main content

Fixing 'Access to fetch has been blocked by CORS policy' in React & Express

 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:

  1. Protocol (e.g., http:// vs https://)
  2. Domain (e.g., localhost or google.com)
  3. Port (e.g., :3000 vs :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.jsserver.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)):

  1. Express intercepts incoming HTTP requests.
  2. It compares the Origin header sent by the browser against your corsOptions.
  3. If they match, Express adds the Access-Control-Allow-Origin: http://localhost:5173 header to the response.
  4. 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:

  1. The server cannot respond with Access-Control-Allow-Origin: *. It must specify the exact origin.
  2. 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:

  1. Understand: The browser blocks cross-origin resource sharing by default.
  2. Install: Use the cors middleware in your Express backend.
  3. Configure: Whitelist your frontend domain in the CORS options.
  4. Authenticate: Set credentials: true on the server and credentials: '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.