CORS (Cross-Origin Resource Sharing) is a browser security feature that blocks web pages from making requests to a different domain than the one that served the page.
If your frontend is on localhost:3000 and your API is on localhost:8080, the browser considers that a cross-origin request and blocks it β unless the API explicitly allows it.
Why CORS exists
Without CORS, any website could make requests to any other website using your cookies. Imagine visiting evil-site.com and it silently makes requests to your-bank.com using your logged-in session. CORS prevents this.
The rule: a web page on domain-a.com can only make requests to domain-a.com. Requests to domain-b.com are blocked unless domain-b.com says βI allow requests from domain-a.com.β
What a CORS error looks like
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.
The browser made the request, the server responded, but the browser refused to show you the response because the server didnβt include the right CORS headers.
How to fix it
The fix is always on the server side. The server needs to include headers that tell the browser βthis origin is allowed.β
Node.js (Express):
const cors = require('cors');
app.use(cors()); // Allow all origins
// Or be specific
app.use(cors({
origin: 'http://localhost:3000',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
Python (FastAPI):
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_methods=["*"],
allow_headers=["*"],
)
Nginx:
location /api/ {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Authorization, Content-Type";
}
For the full fix guide with more examples, see CORS error fix.
How CORS actually works
Simple requests (GET, POST with simple headers): the browser sends the request and checks the response headers.
Preflight requests (PUT, DELETE, custom headers): the browser sends an OPTIONS request first to ask βam I allowed?β If the server says yes, the browser sends the real request.
Browser β OPTIONS /api/users (preflight)
Server β 200 OK + Access-Control-Allow-Origin: http://localhost:3000
Browser β PUT /api/users/42 (actual request)
Server β 200 OK + data
Common CORS scenarios
| Scenario | CORS needed? |
|---|---|
| Frontend and API on same domain | No |
Frontend on localhost:3000, API on localhost:8080 | Yes |
Frontend on app.com, API on api.app.com | Yes (different subdomain) |
| Server-to-server requests | No (CORS is browser-only) |
| Using a proxy in development | No (proxy makes it same-origin) |
The development proxy trick
Instead of configuring CORS, proxy API requests through your dev server:
// vite.config.ts
export default {
server: {
proxy: {
'/api': 'http://localhost:8080',
},
},
};
Now /api/users goes through localhost:3000 (your Vite server) which forwards it to localhost:8080. The browser sees same-origin, no CORS needed.
This only works in development. In production, configure CORS properly on the server.