CORS looks intimidating. People see "Cross-Origin Resource Sharing" and think about domain policies, browser security models, and complex preflight negotiations. They see error messages like "No ‘Access-Control-Allow-Origin’ header is present on the requested resource" and assume there’s some deep magic they don’t understand.
But it’s not magic. CORS is just HTTP headers. Literally. If you understand what headers are, you understand CORS. I’m going to prove it to you by showing you exactly what’s happening at every step, with real requests and responses.
I think you’ll be surprised at how simple it actually is.
The Three Players: Browser, JavaScript Client, Server
Before we dig into anything, let’s be absolutely clear about what each player does:
The Browser = The security enforcer
- Runs your JavaScript code
- Intercepts network requests made by JavaScript
- Checks if the request is allowed based on CORS rules
- Blocks the response from reaching JavaScript if not allowed
- Knows where JavaScript came from (the origin)
The JavaScript Client = The requester
- Runs in the browser on a web page
- Makes HTTP requests to servers
- Receives responses from servers
- Has no idea if the server will allow it
- Doesn’t actually enforce anything
The Server = The gatekeeper
- Receives HTTP requests from anywhere
- Decides which origins are allowed to use its resources
- Sends back HTTP headers telling browsers what’s allowed
- Can choose to allow requests from anywhere or just specific origins
- Doesn’t actually block anything (the browser does)
The key insight: The browser is the enforcer. The server tells it what’s allowed. The JavaScript just makes requests.
The Problem CORS Solves
Let me set the scene. You’re on https://myapp.com. The page loads some JavaScript. That JavaScript does this:
// I'm running on https://myapp.com
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
Seems innocent, right? But imagine if CORS didn’t exist. Imagine any website could:
- Load on your browser
- Make requests to your bank’s API
- Access your data without permission
That would be catastrophic. CORS is the mechanism that says: "Not so fast. If you’re on myapp.com, you can’t just talk to api.example.com without that server explicitly allowing it."
What Actually Happens: The Step-by-Step
Let me show you exactly what happens when CORS comes into play. Here’s a real example.
Step 1: Browser Knows Where JavaScript Came From
Your JavaScript is running on https://myapp.com. The browser knows this. The browser tracks it. The browser remembers: "This JavaScript came from myapp.com."
Step 2: JavaScript Makes a Request to a Different Origin
Your code does:
// Running on: https://myapp.com
// Making request to: https://api.example.com
fetch('https://api.example.com/data')
Notice: myapp.com ≠ api.example.com. These are different origins. This is a cross-origin request.
Step 3: Browser Intercepts the Request
The browser intercepts this request before it goes out on the network. The browser says: "Wait. JavaScript from myapp.com is trying to talk to api.example.com. I need to check if this is allowed."
The browser adds a header to the request:
GET /data HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Connection: keep-alive
...
See that Origin: https://myapp.com header? The browser added that. JavaScript didn’t add it. The browser did.
Step 4: Request Goes to Server
The request travels to api.example.com. The server receives it and sees: "This request is coming from origin https://myapp.com."
Step 5: Server Decides What to Do
Now the server has a choice:
- Allow it: Send back a header saying "yes, origin myapp.com is allowed"
- Block it: Don’t send that header (or send a header saying "no")
Let’s say the server decides to allow it. Here’s the response:
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://myapp.com
...
{"data": "hello world"}
See that Access-Control-Allow-Origin: https://myapp.com header? The server sent that. It’s saying: "I received your request from origin https://myapp.com, and I allow it."
Step 6: Browser Receives Response and Checks
The browser receives this response. It reads the headers and sees: "The server says Access-Control-Allow-Origin is https://myapp.com. And my JavaScript came from https://myapp.com. These match! I’ll let JavaScript see this response."
Step 7: JavaScript Gets the Data
Your code gets the response:
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data)) // "hello world"
Success. No error. Everything worked.
When CORS Blocks a Request: The Error
Let’s trace what happens when CORS blocks a request.
Same setup: JavaScript on https://myapp.com makes a request to https://api.example.com.
The request goes out with the Origin header:
GET /data HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
The server receives it, but decides: "I don’t know https://myapp.com. I’m not allowing it."
The server responds with:
HTTP/1.1 200 OK
Content-Type: application/json
...
{"data": "hello world"}
Notice: No Access-Control-Allow-Origin header.
The browser receives this response and checks: "The server didn’t send an Access-Control-Allow-Origin header. That means the server is NOT allowing this origin. I cannot give this response to JavaScript."
The browser blocks the response from reaching JavaScript and instead throws an error in the console:
Access to XMLHttpRequest at 'https://api.example.com/data' from origin 'https://myapp.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the
requested resource.
JavaScript never sees the response. JavaScript never sees the data. The browser stopped it.
This is what "CORS error" means: The server didn’t include the right header, so the browser blocked the response.
How Servers Fix CORS Errors
So you’re writing a server. You want to allow https://myapp.com to access your API. What do you do?
In Node.js with Express, it’s simple:
// server.js
const express = require('express');
const app = express();
// Middleware to handle CORS
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://myapp.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type');
next();
});
app.get('/data', (req, res) => {
res.json({ data: 'hello world' });
});
app.listen(3000);
That’s it. You’re sending back the Access-Control-Allow-Origin header. The browser sees it, matches it against the JavaScript’s origin, and allows the response through.
Or you could use an existing middleware:
const cors = require('cors');
app.use(cors({
origin: 'https://myapp.com'
}));
app.get('/data', (req, res) => {
res.json({ data: 'hello world' });
});
Same thing, just using a library.
Preflight Requests: When the Browser Gets Extra Cautious
Sometimes the browser doesn’t just send the request. Sometimes it sends two requests.
The first is called a preflight request. It’s the browser asking the server: "Is this request allowed?"
When does this happen? When the request is "complex":
- POST with JSON body
- Requests with custom headers
- Requests other than GET/HEAD/POST
Here’s what it looks like:
Preflight Request (Browser)
OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
The browser is asking: "Can I send a POST request with Content-Type header from origin myapp.com?"
Server Responds to Preflight
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type
The server is saying: "Yes, myapp.com is allowed to send POST requests with Content-Type headers."
Browser Sends Actual Request
Now the browser trusts the server. It sends the actual request:
POST /data HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Content-Type: application/json
{"key": "value"}
Server Responds with CORS Headers
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Content-Type: application/json
{"result": "success"}
The browser allows the response through to JavaScript.
Real Code Examples: Client and Server
Let me show you a real example with actual code on both sides.
Client-Side (JavaScript)
// Running on: https://myapp.com
// Making request to: https://api.example.com
// Simple GET request
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.log('CORS Error:', error));
// POST request with JSON
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: 'value' })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.log('CORS Error:', error));
Notice: The JavaScript doesn’t do anything special. It just makes requests. The browser handles all the CORS logic.
Server-Side (Python Flask)
# server.py
from flask import Flask, jsonify
from flask_cors import CORS
app = Flask(__name__)
# Allow CORS from myapp.com
CORS(app, resources={
r"/*": {
"origins": ["https://myapp.com"],
"methods": ["GET", "POST", "PUT", "DELETE"],
"allow_headers": ["Content-Type"]
}
})
@app.route('/data', methods=['GET', 'POST'])
def get_data():
return jsonify({'result': 'success'})
if __name__ == '__main__':
app.run()
Or manually with headers:
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.route('/data', methods=['GET', 'POST', 'OPTIONS'])
def get_data():
# Handle preflight requests
if request.method == 'OPTIONS':
response = jsonify()
response.headers['Access-Control-Allow-Origin'] = 'https://myapp.com'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
return response
# Handle actual requests
response = jsonify({'result': 'success'})
response.headers['Access-Control-Allow-Origin'] = 'https://myapp.com'
return response
if __name__ == '__main__':
app.run()
Final Thoughts
CORS looks scary but it’s actually elegant. It’s a way for servers to tell browsers: "Hey, I allow requests from these origins." The browser trusts the server and enforces that policy.
When you see a CORS error, it means:
- Your JavaScript on
origin-Atried to accessorigin-B origin-B‘s server didn’t send the right header- The browser blocked the response
To fix it, you need origin-B‘s server to send: Access-Control-Allow-Origin: origin-A
That’s the entire mechanism. HTTP headers. Nothing more.
If you have questions about implementing CORS, debugging CORS errors, or designing APIs with security in mind, I’d love to hear from you. Send me an email or leave a comment.
If you liked this post, please share it! I genuinely appreciate it, and I’d love to know if this made CORS click for you.
If you’re interested in web development, security, and building systems that are both safe and functional, subscribe to my blog at jasonroell.com. I write about this stuff regularly.
Have a great day, and learn on!