What is an origin?
An origin is the scheme, host and port of a web address, taken together. https://birdclub.example is one origin. https://map.birdclub.example is another, because the host differs. http://birdclub.example is a third, because the scheme differs, and http://localhost:3000 and http://localhost:5173 are two more. The path doesn't count: every page on one origin shares it.
Browsers use origins to keep sites apart. A page you open runs its scripts with your cookies, your logins and your network position. If any page could freely read answers from any other site, a quiz game in one tab could read your webmail in another.
The bird club has exactly this setup. The API lives at https://birdclub.example, and a volunteer built the sightings map as a separate site on https://map.birdclub.example. To the browser those are two strangers, even though the same club runs both. The rest of this page is about telling the browser that one stranger is a friend.
Why does the browser block my fetch() to another site?
The same-origin policy is the rule behind it. A script on one origin may send requests to another, but it may not read the answer unless the other server says so. When the bird club's map page at https://map.birdclub.example calls fetch('https://birdclub.example/api/sightings'), the request goes out and your server answers. The browser then looks for permission in the response headers. With none, it throws the answer away and the script gets a network error, with a message about CORS in the console.
This surprises people in two ways. First, the request still reached your server and your route still ran; only the reading was blocked. Second, the block lives in the browser. A command-line tool, another server or the request bar on this page reads the answer fine, because none of them is a browser protecting a visitor.
What does Access-Control-Allow-Origin do?
Cross-Origin Resource Sharing, CORS for short, is the set of headers a server uses to grant that permission. The main one is Access-Control-Allow-Origin. If its value matches the page's origin exactly, or is *, the browser lets the script read the answer.
Choose the value carefully. * means any site may read it, which suits truly public data such as opening hours or a public bird list. For anything else, name the origins you trust. When the answer changes depending on who asked, also send Vary: Origin, so caches don't hand a copy meant for one site to another.
// What the map site's browser needs to see on the answer
Access-Control-Allow-Origin: https://map.birdclub.example
Vary: OriginWhat is a preflight request?
Some requests are risky enough that the browser asks first. A GET, or a POST with a form-style body, goes straight out. A POST with Content-Type: application/json, any PUT, PATCH or DELETE, or a request with custom headers, triggers a preflight: an OPTIONS request to the same path, carrying Origin, Access-Control-Request-Method and Access-Control-Request-Headers.
Your server must answer that preflight with a 2xx status and headers that allow the method, headers and origin. Only then does the browser send the real request. Press **Preflight for a POST** to send exactly what a browser would. Before the fix, Express answers it on its own with 200 and an Allow header, but with no Access-Control- headers, so a browser stops there and the request is never sent. The server can add Access-Control-Max-Age to let the browser remember the answer for a while instead of asking before every call.
- The browser sends
OPTIONS /api/sightingswith the method and headers it wants to use. - Your server answers
204withAccess-Control-Allow-Origin,Access-Control-Allow-MethodsandAccess-Control-Allow-Headers. - The browser checks them, sends the real
POST, and lets the page read the answer if that also carries the origin header.


How do you allow one site with the cors package?
The cors package is middleware that writes these headers and answers preflights for you. Called with no options, cors() allows every origin with *, which is rarely what you want. Pass origin with the one site you trust, or an array of them. With an array, the package compares each request's Origin and only answers with a match.
Mount it before your routes and, where it makes sense, only on the API paths. Here app.use('/api', cors(...)) leaves the club's own HTML pages alone. After the change, compare **From the map site** with **From another site** in the Requests view. With a single string origin, the package sends that same value to everyone, and the quiz site's browser sees a mismatch and blocks it.
const cors = require('cors');
app.use(express.json());
app.use('/api', cors({ origin: 'https://map.birdclub.example' }));
// Several trusted sites:
// cors({ origin: ['https://map.birdclub.example', 'https://birdclub.example'] })What about cookies and logged-in requests?
By default, fetch() to another origin doesn't send cookies. A page that needs them uses credentials: 'include', and your server must then send Access-Control-Allow-Credentials: true with an exact origin. Browsers refuse * together with credentials, on purpose. With the package, that is cors({ origin: 'https://map.birdclub.example', credentials: true }).
Never fix a credentials error by reflecting whatever Origin arrives, as origin: true does. That lets any website make logged-in requests and read the replies, which turns CORS from a fence into an open door. Your cookies' SameSite setting also counts, and it counts sites, not origins. map.birdclub.example and birdclub.example share one site, so a Lax cookie does go with the map page's fetch() when it uses credentials: 'include'. A page on a different site gets no Lax or Strict cookies, whatever the CORS headers say.
Does CORS protect my API?
No. CORS relaxes a browser rule; it never adds protection to your server. Anyone can call your API from outside a browser, and a request that changes data may already have run before the browser decides whether the page may read the answer. Logins, permission checks, validation and rate limits still do the protecting. Think of CORS as deciding which other websites may use your API from a visitor's browser.
If both the page and the API live on the same origin, for example the page served from public and fetch('/api/sightings'), you need no CORS at all. Prefer that layout when you control both.
How do you check CORS, and what do AI tools get wrong?
Requests from the request bar have no Origin header, so use the Try buttons that add one, or add an Origin header yourself, then read the Access-Control- headers under “Headers and body”. On your own computer, the browser console names the missing header, and the Network panel shows the preflight as a separate OPTIONS row.
AI tools often reach for app.use(cors()) with no options, which opens the API to every site, or suggest origin: true with credentials: true to silence an error. They also forget that an origin includes the scheme and port, or put the middleware after the routes. Ask which sites should be allowed, and write them down by name.
Field notes
- An origin is the scheme, host and port of a web address taken together.
- Browsers stop pages from reading answers from another origin unless that server allows it.
Access-Control-Allow-Originnames the origin that may read the answer.- A JSON
POSTfrom another origin first sends anOPTIONSpreflight request.
Easy to get wrong
With no options the package sends Access-Control-Allow-Origin: *, so every website's scripts may read the API. Name the sites you trust.
Middleware runs in the order you add it. A route that answers ends the request, so CORS headers added after it never reach the response.
origin: true echoes back any origin, which with credentials lets any website make logged-in requests and read the answers.
Questions people ask
Why does my API work in the request bar but not from the other site's page?
The same-origin policy is enforced by browsers. The request bar, command-line tools and other servers don't apply it. A page in a browser only gets the answer if your server sends the right CORS headers.
Do I need CORS for my own pages calling my own API?
Not if they share an origin, meaning the same scheme, host and port. A page served by your Express app that calls fetch('/api/...') is same-origin and needs nothing extra.
Can I write the headers myself instead of using the package?
Yes. A small middleware can check req.get('origin') against an allowed list, set Access-Control-Allow-Origin and Vary: Origin, and answer OPTIONS with 204 and the allowed methods and headers. The package does the same and handles the edge cases.
Is CORS the same as protection against cross-site request forgery?
No. Forged form posts don't need to read the answer, so CORS doesn't stop them. SameSite cookies, checking the Origin header on changes and tokens in forms are the defences there.