Ever deployed your Node.js API only to see Access to fetch has been blocked by CORS policy in the browser console? You’re not alone. CORS errors are among the most common frustrations for backend developers, yet the underlying mechanism is often misunderstood. Let’s fix that.
The Foundation: Same-Origin Policy
Browsers enforce a security mechanism called the Same-Origin Policy (SOP). It prevents JavaScript running on https://myapp.com from making requests to https://api.example.com unless explicitly allowed.
Origin = Protocol + Domain + Port
https://api.example.com:443→ Origin Ahttps://api.example.com:3000→ Origin B (different port)http://api.example.com:443→ Origin C (different protocol)
Without SOP, malicious scripts on evil.com coul…
Ever deployed your Node.js API only to see Access to fetch has been blocked by CORS policy in the browser console? You’re not alone. CORS errors are among the most common frustrations for backend developers, yet the underlying mechanism is often misunderstood. Let’s fix that.
The Foundation: Same-Origin Policy
Browsers enforce a security mechanism called the Same-Origin Policy (SOP). It prevents JavaScript running on https://myapp.com from making requests to https://api.example.com unless explicitly allowed.
Origin = Protocol + Domain + Port
https://api.example.com:443→ Origin Ahttps://api.example.com:3000→ Origin B (different port)http://api.example.com:443→ Origin C (different protocol)
Without SOP, malicious scripts on evil.com could steal data from your bank’s API while you’re logged in. SOP blocks this by default.
What Is CORS?
Cross-Origin Resource Sharing (CORS) is the protocol that allows servers to relax SOP restrictions. It’s a set of HTTP headers that tells browsers: "Yes, I trust requests from this external origin."
Critical insight: CORS is enforced by browsers, not servers. Your Node.js API receives and processes the request regardless. The browser decides whether to expose the response to your frontend JavaScript based on CORS headers.
How CORS Works (Simple Requests)
For basic GET or POST requests with standard headers, the browser:
- Sends the request with an
Originheader - Server responds with
Access-Control-Allow-Origin: *(or specific origin) - Browser checks the header and either allows or blocks JavaScript access to the response
import express from 'express';
const app = express();
app.get('/api/data', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.json({ message: 'CORS enabled for myapp.com' });
});
app.listen(3000);
What Is a Preflight Request?
For non-simple requests, browsers send an OPTIONS request before the actual request. This is the preflight.
When Does a Preflight Trigger?
A preflight occurs when your request includes:
- Custom headers (e.g.,
Authorization,X-API-Key) - HTTP methods other than GET, HEAD, or POST
- Content-Type other than
application/x-www-form-urlencoded,multipart/form-data, ortext/plain
Example: A PUT request with Content-Type: application/json triggers a preflight.
The Preflight Flow
- Browser sends OPTIONS request:
OPTIONS /api/users/123
Origin: https://myapp.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type
- Server responds with permissions:
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: PUT, DELETE
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400
- If approved, browser sends actual PUT request
Handling Preflight in Express.js
import express from 'express';
const app = express();
// Handle preflight for all routes
app.options('*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '86400'); // Cache preflight for 24 hours
res.sendStatus(204);
});
// Actual endpoint
app.put('/api/users/:id', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.json({ updated: true });
});
app.listen(3000);
Pro tip: Use the cors npm package in production to avoid repetitive header management:
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors({
origin: 'https://myapp.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
}));
app.listen(3000);
Common CORS Mistakes
Setting Access-Control-Allow-Origin: * with credentials → Browsers reject this. Use specific origins when sending cookies or auth tokens.
1.
Forgetting to handle OPTIONS → Preflight requests fail silently, and developers blame CORS instead of missing OPTIONS handlers. 1.
Server-side CORS logic → Remember: CORS headers tell the browser what to allow. Your server already processed the request.
Key Takeaways
- Same-Origin Policy protects users; CORS allows controlled exceptions
- Browsers enforce CORS, not servers
- Preflight requests (OPTIONS) occur for non-simple requests to verify permissions before the actual request
- Always handle OPTIONS explicitly in production APIs
- Use the
corspackage to simplify header management in Node.js
CORS errors feel frustrating because they happen after your server processed the request. Understanding the browser–server handshake transforms debugging from guesswork into systematic problem-solving.
What CORS challenge have you faced in production? Share your experience below!