Only show guides for your app
Why the browser blocks your API call with a CORS error, what the preflight request actually is, and the exact one-line fix with the cors package — reproduced with real requests.
You built an API, it works in curl, and then your website's JavaScript calls it and the browser console screams:
Access to fetch at 'http://your-api' from origin 'https://your-site'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header
is present on the requested resource.
Nothing is broken. Your API is fine — curl proves it. CORS is a browser safety rule, and this error means your API simply hasn't said "yes, that other website may call me." This guide explains what's really happening and gives the exact fix.
| At a glance | |
|---|---|
| You need | An Express API on the Node.js application |
| Background | Designing a small REST API |
| Plan | Any plan |
| Time | Fifteen minutes |
CORS — Cross-Origin Resource Sharing — is a rule browsers enforce, and only browsers. When JavaScript on https://your-site tries to fetch from http://your-api (a different origin — different domain, or even a different port), the browser refuses to hand the response back to your code unless the API explicitly allows that origin with a response header.
Two things follow from that:
200; the browser just won't let the page read it.curl, Postman, your bot's backend, and server-to-server calls don't do CORS at all. That's why the API "works everywhere except the browser."An origin is the scheme + host + port together. https://site.com, http://site.com, and https://site.com:8080 are three different origins — a mismatch in any of them triggers CORS.
For anything beyond a simple GET (a POST with a JSON body, a custom header, a DELETE), the browser sends a preflight first: an automatic OPTIONS request that asks "am I allowed to do this?" Your API must answer with the right Access-Control-* headers, or the real request never leaves the browser.
You can watch the difference with curl by imitating what the browser sends. Against a plain Express API with no CORS setup, the preflight comes back naked:
curl -i -X OPTIONS http://YOUR_API/api/data \
-H "Origin: https://your-site" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type"
HTTP/1.1 200 OK
No Access-Control-* headers in the response — so a real browser stops right there and logs the error. That empty answer is the bug.
The cors middleware adds those headers for you. Install it from the Packages page (search cors, install, restart), then add one line:
const express = require('express');
const cors = require('cors');
const app = express();
// Allow exactly your site. This one line answers preflights too.
app.use(cors({ origin: 'https://your-site' }));
app.use(express.json());
app.get('/api/data', (req, res) => res.json({ message: 'hello' }));
app.post('/api/data', (req, res) => res.json({ ok: true }));
const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));
Run the exact same preflight now and the answer changes completely:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://your-site
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Headers: content-type
The browser sees Access-Control-Allow-Origin matches its origin, the method is allowed, and lets the real request through. A normal GET now carries Access-Control-Allow-Origin: https://your-site too. That's the whole fix.
🎯 Good to know:
app.use(cors(...))must come before your routes, so the middleware runs first and handles the automaticOPTIONSpreflight. Put it near the top, right after you createapp.
cors() with no options allows every origin (Access-Control-Allow-Origin: *). That's fine for a genuinely public, read-only API — and wrong for anything private, because it invites every website to call yours.
| You want | Use |
|---|---|
| A public, read-only API | app.use(cors()) — allow all |
| Only your own site | app.use(cors({ origin: 'https://your-site' })) |
| A few known sites | origin: ['https://a.com', 'https://b.com'] |
| Cookies/sessions to cross origins | cors({ origin: 'https://your-site', credentials: true }) and fetch(url, { credentials: 'include' }) on the client |
⚠️ Heads up: You cannot combine
credentials: truewith the*wildcard — the browser forbids it. Credentialed requests require naming the exact origin. This is the usual wall when a logged-in session needs to work across domains.
CORS only exists because the site and the API are on different origins. Serve both from the same Falix server and the problem disappears — no cors package, no preflights. Express is happy to serve your web page and your API routes from one app on your one SERVER_PORT, exactly as Build an Express website + API shows. If a single build serves your frontend and backend together, you never meet CORS at all.
For the standard behind all of this, MDN's CORS reference is the canonical explainer, and the middleware's own docs are at github.com/expressjs/cors.
cors — the allowed origin string must match the browser's origin exactly, including http vs https and any port. Copy it verbatim from the error message.app.use(cors(...)) runs before your routes so OPTIONS gets answered.credentials: true on the server and a named (non-wildcard) origin and credentials: 'include' on the client fetch.https://.... Update the allowed origin to match (Domains and HTTPS).