Cross-Origin Resource Sharing, or CORS, is one of those web technologies that many developers hear about only when something breaks. You might be building a new frontend, connecting to your API, and suddenly your browser throws that dreaded red error:
“Access to fetch at ‘https://api.example.com’ from origin ‘https://frontend.example.com’ has been blocked by CORS policy.”
For most developers, the immediate response is to jump into Stack Overflow and paste Access-Control-Allow-Origin: * somewhere on the server. It seems to work, and everyone moves on. But very few people stop to ask:
What’s actually happening behind the scenes when your browser enforces CORS?
In this article, we’ll peel back the layers and understand the logic that powers CORS — from HTTP requests to browser policies and server responses. We’ll also explore how different backend technologies handle CORS, how preflight requests work, and what security trade-offs exist when you configure CORS incorrectly.
The Origin Story
To understand CORS, we must first go back to the same-origin policy, the foundation of web security.
Every web page has an origin, defined by three parts:
Protocol (http or https)
Domain name (e.g., example.com)
Port (e.g., :80 or :443)
Two URLs are considered the same origin only if all three parts match.
For instance:
https://nilebits.com and https://nilebits.com:443 → same origin
https://blog.nilebits.com and https://nilebits.com → different origins
http://nilebits.com and https://nilebits.com → different origins
The same-origin policy was created to protect users. Imagine if a malicious website could silently make requests to your bank’s API and read sensitive data just because you’re logged in — that would be disastrous.
However, as the web evolved, legitimate cases appeared where developers needed to make cross-origin requests, such as calling an API hosted on another domain.
That’s where CORS came in — as a controlled relaxation of the same-origin policy.
What CORS Actually Does
CORS doesn’t change the fact that browsers enforce the same-origin policy. Instead, it provides a negotiation mechanism between the browser and the server.
It allows the server to tell the browser:
“It’s okay, this domain is allowed to access my resources.”
This is done through HTTP headers.
Let’s visualize a simple example.
A normal request
You’re on https://frontend.nilebits.com, and your JavaScript code tries to fetch data from https://api.nilebits.com.
fetch('https://api.nilebits.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
When this code runs, the browser sees that frontend.nilebits.com and api.nilebits.com have different origins. So, it applies the CORS policy.
Behind the scenes, your browser sends something like:
GET /data HTTP/1.1
Host: api.nilebits.com
Origin: https://frontend.nilebits.com
Now the server must decide whether to allow or reject the request. If it responds with:
Access-Control-Allow-Origin: https://frontend.nilebits.com
Then the browser will allow your JavaScript to read the response.
If that header is missing or doesn’t match the origin, the browser will block the response — even though the server technically sent it.
Preflight Requests Explained
Some types of requests are considered simple by CORS standards — typically GET, HEAD, or POST with safe content types like application/x-www-form-urlencoded, multipart/form-data, or text/plain.
Other requests are non-simple, meaning they can potentially change server state or carry custom headers. For those, browsers send an extra request before the actual one — called a preflight request.
Here’s how it looks:
OPTIONS /data HTTP/1.1
Host: api.nilebits.com
Origin: https://frontend.nilebits.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
The server must reply with something like:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.nilebits.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 3600
This tells the browser it’s safe to proceed with the real request.
If the preflight response is missing or incorrect, the browser blocks the main request.
Preflight requests are invisible in your JavaScript code — they happen automatically before your main request is sent.
CORS in Action: Frontend Example
Let’s demonstrate what happens in real code. Suppose you have this frontend:
<!DOCTYPE html>
CORS Demo
Load Data
<br>
document.getElementById('load').addEventListener('click', () => {<br>
fetch('<a href="https://api.nilebits.com/data">https://api.nilebits.com/data</a>', {<br>
headers: {<br>
'Authorization': 'Bearer abc123'<br>
}<br>
})<br>
.then(response => response.json())<br>
.then(data => console.log(data))<br>
.catch(err => console.error('CORS Error:', err));<br>
});<br>
If the backend at api.nilebits.com doesn’t include the correct CORS headers, you’ll see something like:
Access to fetch at 'https://api.nilebits.com/data' from origin 'https://frontend.nilebits.com' has been blocked by CORS policy.
CORS on the Server Side (Node.js Example)
Let’s now see what happens when you configure CORS on your backend.
Using Express and the cors middleware:
const express = require('express');
const cors = require('cors');
const app = express();
const allowedOrigins = ['https://frontend.nilebits.com'];
app.use(cors({
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
app.get('/data', (req, res) => {
res.json({ message: 'Hello from Nile Bits API' });
});
app.listen(3000, () => console.log('Server running on port 3000'));
Here, we only allow the frontend at https://frontend.nilebits.com.
If a request comes from another origin, it’s blocked.
CORS in .NET (C# Example)
In ASP.NET Core, CORS can be configured globally or per controller.
Here’s an example of adding CORS middleware in your Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend",
policy => policy.WithOrigins("https://frontend.nilebits.com")
.AllowAnyHeader()
.AllowAnyMethod());
});
var app = builder.Build();
app.UseCors("AllowFrontend");
app.MapGet("/data", () => new { Message = "Hello from .NET Nile Bits API" });
app.Run();
CORS in Python (Flask Example)
In Python Flask, the simplest way is to use the flask-cors package.
from flask import Flask, jsonify
from flask_cors import CORS
app = Flask(name)
CORS(app, origins=["https://frontend.nilebits.com"])
@app.route('/data')
def data():
return jsonify(message="Hello from Nile Bits Flask API")
if name == 'main':
app.run()
What Happens Behind the Scenes: A Timeline
Let’s map out what happens step by step when your JavaScript makes a cross-origin request.
JavaScript executes fetch() → The browser checks the URL’s origin.
CORS check begins → If origins differ, browser adds an Origin header.
If simple request → Browser sends it directly with Origin.
If non-simple → Browser sends an OPTIONS preflight request first.
Server validates and responds with CORS headers.
Browser validates those headers and either allows or blocks the real request.
JavaScript receives the response only if the browser approves it.
The crucial point here is that CORS is enforced by browsers, not servers.
A curl command or Postman request won’t trigger a CORS error — because they’re not subject to browser security models.
Common Misunderstandings About CORS
“CORS is a server issue.”
Not exactly. CORS is a browser enforcement mechanism. The server just declares its intentions.
“Using Access-Control-Allow-Origin: * is safe.”
It’s fine for public APIs, but dangerous if your endpoints expose sensitive data or use credentials.
“Disabling CORS in the browser is a solution.”
It might help during local development, but never in production. You’re effectively removing a security layer.
“CORS is the same as authentication.”
No. CORS controls who can access, not who is logged in. It doesn’t replace tokens or authentication systems.
Credentials and CORS
By default, browsers don’t send cookies or authorization headers with cross-origin requests.
To enable that, you need:
Frontend
fetch('https://api.nilebits.com/data', {
credentials: 'include'
});
Backend
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://frontend.nilebits.com
You can’t use * when Allow-Credentials is true — the browser will reject it.
Debugging CORS Issues
Debugging CORS errors can be frustrating. Here’s a quick checklist:
Open the Network tab in browser dev tools. Check the OPTIONS preflight request.
Make sure the response headers include:
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Check whether your request includes credentials: true — and whether your server supports it.
Always test using an actual browser — Postman won’t reveal CORS problems.
For reference, check the official MDN CORS documentation.
Security Considerations
CORS can open security holes if configured too loosely.
Common mistakes:
Allowing * for all origins and credentials.
Reflecting the Origin header without validation.
Forgetting to restrict allowed methods or headers.
A well-configured CORS policy is part of your API’s defense surface.
Real-World Use Cases
At Nile Bits, when building microservice architectures, we often host frontend apps (React or NextJS) on one subdomain and APIs on another.
For instance:
Frontend: https://app.nilebits.com
Proper CORS setup becomes essential.
We typically:
Allow only specific origins (our production domains).
Use strict header whitelisting.
Enforce HTTPS and authentication tokens.
This approach balances security and usability.
You can read more about our modern API design approach in our article Understanding Modern API Architectures: Best Practices and Real-World Examples.
The W3C Standard View
The CORS specification is defined by the W3C Fetch Standard. It describes how browsers must handle cross-origin requests, including caching, preflights, and exposed headers.
A key part of the spec is exposed response headers.
By default, only a few headers are visible to frontend JavaScript:
Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, and Pragma.
If you want your API to expose custom headers like X-RateLimit-Remaining, you must include:
Access-Control-Expose-Headers: X-RateLimit-Remaining
Deep Dive: Preflight Caching
Browsers cache successful preflight responses for efficiency. The header:
Access-Control-Max-Age: 3600
tells the browser to reuse the preflight result for one hour.
This optimization can drastically reduce latency when your frontend makes frequent calls.
Behind the Browser Curtain: Internal Logic
Let’s look at how browsers internally process CORS.
The network stack receives a request from JavaScript.
It checks the URL’s scheme, host, and port.
If the origin differs, it checks cache for preflight permission.
If no cached result exists, it sends an OPTIONS request.
The server replies with headers — browser validates them.
The network layer updates the internal CORS permission store.
The main request proceeds.
Response headers are filtered to expose only allowed ones.
This flow happens automatically in milliseconds.
Testing and Mocking CORS in Local Development
When developing locally, CORS can become annoying because your frontend (http://localhost:3000) and backend (http://localhost:5000) are different origins.
Solutions:
Configure your backend to allow http://localhost:3000.
Use a proxy in development (like in React’s package.json): "proxy": "http://localhost:5000"
Or run a browser with CORS disabled temporarily (for debugging only).
Advanced Example: Dynamic CORS Validation
Sometimes you want to allow dynamic origins stored in a database.
app.use(cors({
origin: async (origin, callback) => {
const allowed = await db.isAllowedOrigin(origin);
if (allowed) callback(null, true);
else callback(new Error('Blocked by CORS'));
}
}));
This ensures only trusted partners can use your API.
CORS and APIs at Scale
Large platforms like Stripe or GitHub use CORS carefully. Their APIs serve both browser-based and server-based clients.
To balance security:
They separate public and private endpoints.
Public endpoints allow * for read-only access.
Authenticated ones restrict specific domains.
That’s a model many modern SaaS APIs follow — and something Nile Bits often recommends to clients building global-scale APIs.
Wrapping Up
CORS isn’t just a technical annoyance. It’s an elegant negotiation protocol between browsers and servers that keeps the web safe.
When you understand what happens behind the scenes — from the Origin header to preflight caching — you gain control over how your frontend and backend communicate securely.
At Nile Bits, we always treat CORS as part of our API design strategy, not an afterthought. It’s one of the subtle yet powerful layers that enable modern web applications to operate across domains without compromising security.
If you found this breakdown helpful, explore more of our deep technical insights at Nile Bits Blog.
You might also like our detailed guide Deploying React Apps: A Guide to Using GitHub Pages for frontend developers.
Top comments (0)