CORS — Cross-Origin Resource Sharing

You've built a frontend on localhost:5173 that calls an API on localhost:3000. It works in Postman. It works with curl. But in the browser, you get this:

Access to fetch at 'http://localhost:3000/api/data' from origin
'http://localhost:5173' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

This is the most common error in modern web development, and it's not a bug. The browser is doing exactly what it's supposed to.

Why CORS Exists

Browsers enforce a same-origin policy: JavaScript on one origin (domain + port + protocol) can't read responses from a different origin. Without this, any website you visit could silently make requests to your bank, email, or internal company tools using your logged-in session cookies.

An "origin" is defined as the combination of protocol + domain + port. So these are all different origins:

URLOrigin
http://localhost:5173http://localhost:5173
http://localhost:3000http://localhost:3000 ← different port!
https://localhost:5173https://localhost:5173 ← different protocol!
http://127.0.0.1:5173http://127.0.0.1:5173 ← different domain!

Yes, localhost:5173 and localhost:3000 are different origins. So are localhost and 127.0.0.1. This is why your frontend can't call your backend even though they're both on your machine.

The Fix: Add CORS Headers

The server (your API) needs to send a response header telling the browser "it's okay, this origin is allowed to read my responses." The header is Access-Control-Allow-Origin.

Express.js (Node)

// npm install cors
const cors = require('cors');
const app = require('express')();

// Allow all origins (development only!)
app.use(cors());

// Or allow specific origin
app.use(cors({
    origin: 'http://localhost:5173',
    credentials: true  // if sending cookies
}));

Django (Python)

# pip install django-cors-headers

# settings.py
INSTALLED_APPS = [..., 'corsheaders', ...]
MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware', ...]

# Allow specific origins
CORS_ALLOWED_ORIGINS = ['http://localhost:5173']

# Or allow all (development only!)
CORS_ALLOW_ALL_ORIGINS = True

Flask (Python)

# pip install flask-cors
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # Allow all origins

# Or specific origin
CORS(app, origins=['http://localhost:5173'])

Laravel (PHP)

// config/cors.php (Laravel 7+)
return [
    'paths' => ['api/*'],
    'allowed_origins' => ['http://localhost:5173'],
    'allowed_methods' => ['*'],
    'allowed_headers' => ['*'],
    'supports_credentials' => true,
];

Nginx (Reverse Proxy)

# Add to server or location block
add_header Access-Control-Allow-Origin "http://localhost:5173";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";

# Handle preflight OPTIONS requests
if ($request_method = OPTIONS) {
    return 204;
}

Preflight Requests

For "complex" requests (anything other than a simple GET/POST with basic headers), the browser sends a preflight request — an OPTIONS request to the server asking "are you okay with this?" before sending the actual request. Your server must handle OPTIONS requests and respond with the appropriate CORS headers.

You'll see two requests in your browser's Network tab: first an OPTIONS request, then the actual GET/POST. If the OPTIONS request fails or doesn't return the right headers, the real request never fires.

The Proxy Approach (No CORS Headers Needed)

Instead of adding CORS headers to your API, many frontend dev servers can proxy API requests. This avoids CORS entirely because the browser only talks to one origin:

// vite.config.js
export default {
    server: {
        proxy: {
            '/api': {
                target: 'http://localhost:3000',
                changeOrigin: true,
            }
        }
    }
}

Now your frontend calls /api/data (same origin, no CORS) and Vite proxies it to localhost:3000/api/data. This only works in development — in production, your reverse proxy (Nginx) handles the routing.

Common Mistakes