The Problem
During local development, your frontend (e.g., http://localhost:3000) makes API requests to a backend on a different port (e.g., http://localhost:8080). The browser blocks these requests due to CORS (Cross-Origin Resource Sharing) policy, even though both are on localhost.
Symptoms
- Console error: "Access to fetch has been blocked by CORS policy"
- Network tab shows the request completes with status 200
- Response is empty or undefined in JavaScript
- Works with curl or Postman but not from the browser
Real Error Message
Access to fetch at 'http://localhost:8080/api/users' from origin
'http://localhost:3000' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested
resource. If an opaque response serves your needs, set the request's
mode to 'no-cors' to fetch the resource with CORS disabled.Why CORS Applies to Localhost
CORS is enforced based on origin (protocol + host + port). http://localhost:3000 and http://localhost:8080 are DIFFERENT origins because the ports differ.
How to Fix It
Fix 1: Add CORS Headers to Backend (Node.js/Express)
```javascript const cors = require('cors'); const express = require('express'); const app = express();
// Allow all origins (development only!) app.use(cors({ origin: 'http://localhost:3000', credentials: true }));
app.get('/api/users', (req, res) => { res.json({ users: [] }); }); ```
Fix 2: Use Dev Server Proxy (Vite)
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false
}
}
}
});Now the frontend calls /api/users (same origin) and Vite proxies it to the backend.
Fix 3: Use Dev Server Proxy (Create React App)
// package.json
{
"proxy": "http://localhost:8080"
}Fix 4: Use Dev Server Proxy (Webpack)
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': 'http://localhost:8080'
}
}
};Fix 5: Temporary Browser Flag (Development Only)
```bash # Chrome (DISABLES ALL CORS - dangerous!) chrome --disable-web-security --user-data-dir=/tmp/chrome-dev
# NEVER use this for regular browsing ```
Fix 6: Handle Preflight OPTIONS Requests
// Backend must respond to OPTIONS preflight
app.options('/api/*', (req, res) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
res.sendStatus(204);
});Fix 7: Debug CORS with curl
```bash # Test preflight curl -X OPTIONS http://localhost:8080/api/users \ -H "Origin: http://localhost:3000" \ -H "Access-Control-Request-Method: GET" \ -H "Access-Control-Request-Headers: Authorization" \ -v
# Check response headers curl -v http://localhost:8080/api/users \ -H "Origin: http://localhost:3000" ```