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:
| URL | Origin |
|---|---|
| http://localhost:5173 | http://localhost:5173 |
| http://localhost:3000 | http://localhost:3000 ← different port! |
| https://localhost:5173 | https://localhost:5173 ← different protocol! |
| http://127.0.0.1:5173 | http://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
- "I set the headers but it still doesn't work" — Check that your server responds to OPTIONS requests, not just GET/POST. Many frameworks need explicit OPTIONS handling
- Using
*with credentials —Access-Control-Allow-Origin: *andAccess-Control-Allow-Credentials: truecan't be used together. You must specify the exact origin when credentials are involved - "It works in Postman but not the browser" — Postman doesn't enforce CORS. Browsers do. CORS is a browser-only security feature
- Disabling CORS in the browser — Don't. Some people install browser extensions to bypass CORS for development. This masks the problem and you'll hit it again in production