Storage and browser security
CORS, Cross-Origin Resource Sharing
Learn how CORS response headers let browser JavaScript read cross-origin responses, how preflight and credentials work, and how to configure Express.
CORS, or Cross-Origin Resource Sharing, is a set of HTTP headers that lets a server say which other origins may read its responses in a browser.
These are four different origins:
https://app.example.comhttps://api.example.comhttp://app.example.comhttps://app.example.com:8443
A fetch() or XHR call can send a cross-origin request, but the same-origin policy hides the response from JavaScript. Unless the server returns the right CORS headers.
So the server owns the policy. You can’t fix a missing CORS header from frontend code.
If you’re stuck right now, I built a free CORS debugger that tells you which headers the server needs. The complete rules are in the MDN CORS guide.
What does CORS protect?
CORS controls whether browser JavaScript can read a response. It doesn’t stop the request from reaching the server.
curl or another server can still call your API. CORS is not authentication or authorization.
Images and classic scripts embed without CORS. Web fonts, JavaScript modules, WebGL textures, and Canvas reads follow CORS rules.
Allow a public resource
For public data with no credentials:
Access-Control-Allow-Origin: *
Any origin may now read the response. Never do this for private data.
Allow one origin
To allow one frontend, return its exact origin:
Access-Control-Allow-Origin: https://app.example.com
If the server picks the value from an allowlist, also send:
Vary: Origin
This tells caches the response depends on Origin. See the Access-Control-Allow-Origin reference. Never reflect an incoming Origin without checking the allowlist.
Example with Express
Use the official cors middleware. This route allows one frontend:
const express = require('express')
const cors = require('cors')
const app = express()
const corsOptions = {
origin: 'https://app.example.com',
}
app.get('/products', cors(corsOptions), (request, response) => {
response.json([{ id: 1, name: 'Keyboard' }])
})
app.listen(3000)
Same policy for the whole API? Register it once:
app.use(cors(corsOptions))
Registered this way, it also answers preflight requests.
How preflight requests work
A simple request goes out unchecked: GET, HEAD, or POST with only safelisted headers. For POST, the allowed Content-Type values are:
application/x-www-form-urlencodedmultipart/form-datatext/plain
Anything else, and the browser first asks permission with an OPTIONS request, the preflight. Even a GET triggers one if you add an Authorization header:
OPTIONS /products HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization
The server approves with:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: DELETE
Access-Control-Allow-Headers: Authorization
Only then does the browser send the real DELETE.
CORS with cookies
fetch() leaves cookies out of cross-origin requests by default. To include them:
fetch('https://api.example.com/account', {
credentials: 'include',
})
The server must then return both:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Credentials and Access-Control-Allow-Origin: * don’t mix. CORS also doesn’t override SameSite or Secure, and it doesn’t replace CSRF protection.
Debugging CORS errors
JavaScript gets a generic network error. The browser console has the details. Check:
- the exact frontend origin
Access-Control-Allow-Originon the response- the
OPTIONSresponse for preflighted requests - allowed methods and headers
- credential settings on both sides
Don’t reach for mode: 'no-cors'. It gives you an opaque response with no status, headers, or body. It silences the error, nothing more.
Lesson completed