Skip to content
FLAVIO COPES
flaviocopes.com
2026

CORS, Cross-Origin Resource Sharing

By

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 an HTTP-header mechanism that lets a server choose which other origins may read its responses in a browser.

An origin is the combination of scheme, host, and port. These are different origins:

Browser JavaScript normally follows the same-origin policy. A fetch() or XHR call can send a cross-origin request, but JavaScript cannot read the response unless the server returns the right CORS headers.

The server controls the policy. You cannot fix a missing CORS header only in frontend JavaScript.

If you’re stuck on a CORS error right now, I built a free CORS debugger: describe your request and it tells you which headers the server needs to send.

The complete rules live in the MDN CORS guide.

What does CORS protect?

CORS controls whether browser JavaScript can read a response. It does not stop a request from reaching the server.

This distinction matters.

A form, curl, another server, or a script outside the browser can still send a request to your API. CORS is not authentication or authorization. You must still protect private routes and check permissions on the server.

Some cross-origin resources can be embedded without CORS, including many images and classic scripts. Reading those resources from JavaScript is different. Web fonts, JavaScript modules, WebGL textures, and images read through Canvas also use CORS rules.

Allow a public resource

For a public response that does not use credentials, the server can return:

Access-Control-Allow-Origin: *

The wildcard means any origin may read the response in browser JavaScript.

Do not use this for private data. The browser does not know whether a response is sensitive.

Allow one origin

To allow one frontend, return its exact origin:

Access-Control-Allow-Origin: https://app.example.com

If the server chooses the value dynamically from an allowlist, also return:

Vary: Origin

This tells caches that the response can change based on the request’s Origin header. See the Access-Control-Allow-Origin reference.

Never reflect any incoming Origin without checking it against an allowlist first.

Example with Express

For Express, use the official cors middleware.

This route allows one frontend origin:

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)

The middleware sets the response headers. It does not block non-browser clients from calling the route.

If the same policy applies to the whole API, register it once:

app.use(cors(corsOptions))

Application-level middleware also handles preflight requests for the matching routes.

How preflight requests work

Some cross-origin requests can be sent without a preflight. They must use GET, HEAD, or POST, and only CORS-safelisted headers.

For POST, the allowed Content-Type values are:

Even a GET can trigger preflight if you add a non-safelisted header such as Authorization.

For other requests, the browser first sends an OPTIONS request. This asks whether the real method and headers are allowed:

OPTIONS /products HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization

The server can approve it 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

The browser sends the DELETE request only after the preflight succeeds.

CORS with cookies

Fetch does not include cross-origin credentials by default. To send cookies, use:

fetch('https://api.example.com/account', {
  credentials: 'include',
})

The server must return both:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

You cannot combine credentials with Access-Control-Allow-Origin: *. The browser will block access to that response.

CORS also does not override cookie rules such as SameSite and Secure.

CORS does not replace CSRF protection. If a route changes data using cookies, protect it against cross-site request forgery too.

Debugging CORS errors

JavaScript usually receives a generic network error when CORS fails. The browser console contains the useful details.

Check:

Avoid reaching for mode: 'no-cors' as a fix. It gives JavaScript an opaque response whose status, headers, and body cannot be read.

~~~

Related posts about platform: