Suggest an editImprove this articleRefine the answer for “What is CORS and how does it work?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**CORS (Cross-Origin Resource Sharing)** is a browser mechanism that uses HTTP headers to control which origins can read server responses. The server sets `Access-Control-Allow-Origin`; the browser checks it before giving the response to JavaScript. Non-simple requests trigger a preflight `OPTIONS` check first. Credentials require an exact origin, never `*`.Shown above the full answer for quick recall.Answer (EN)Image**CORS (Cross-Origin Resource Sharing)** is a browser security mechanism that uses HTTP headers to let servers control which origins can read their responses. ## Theory ### TL;DR - Browsers block cross-origin reads by default (same-origin policy). CORS lets servers opt in via response headers. - The key header is `Access-Control-Allow-Origin`. If it matches the requesting origin, the browser releases the response to JavaScript. - For complex requests (custom headers or non-standard methods), the browser sends a preflight `OPTIONS` request first. - Credentials (cookies, auth tokens) require `Access-Control-Allow-Credentials: true` plus a specific origin, never `*`. - Node.js does not enforce CORS. Only browsers do. ### Quick example ```js // Browser on http://localhost:3000 fetches from a different origin fetch('https://api.example.com/data') .then(r => r.json()) .catch(e => console.error(e)); // Error: No 'Access-Control-Allow-Origin' header is present // Works when server responds with: // Access-Control-Allow-Origin: http://localhost:3000 ``` The browser blocks the response, not the request itself. The request reaches the server, but the browser holds the response until it checks the headers. ### Same-origin policy Two URLs share an origin only if their scheme, host, and port all match. `http://localhost:3000` and `http://localhost:3001` are different origins even though both run on localhost. CORS does not replace same-origin policy. It adds an opt-in layer on top. The server says "yes, this origin is allowed," and the browser respects that. ### Preflight requests Not every cross-origin request goes straight through. If a request uses `PUT`, `DELETE`, or `PATCH`, or sends a non-standard header like `Authorization` or `Content-Type: application/json`, the browser sends an `OPTIONS` request first. That preflight asks: "I am about to send this kind of request from this origin. Is that allowed?" The server responds with `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers`. If those match, the real request follows. Simple requests (GET, POST, HEAD with standard headers and no JSON body) skip preflight entirely. ### When to use - Public API open to everyone: `Access-Control-Allow-Origin: *` - App talking to its own API: `Access-Control-Allow-Origin: https://app.yourcompany.com` - Requests that send cookies or auth tokens: `Access-Control-Allow-Credentials: true` plus the exact origin, not `*` - Non-simple requests: handle the `OPTIONS` preflight manually or use the `cors` npm package ### Common mistakes **Using `*` with credentials** ```js // Wrong - the browser rejects this combination res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Credentials', 'true'); // FAIL // Fix - use the exact origin res.header('Access-Control-Allow-Origin', req.get('Origin')); res.header('Access-Control-Allow-Credentials', 'true'); ``` The browser throws: "Credential is not supported if the CORS header is `*`". This is the most common CORS error in production and a classic interview question. **Missing OPTIONS handler** ```js // Wrong - PUT triggers preflight, but there is no OPTIONS route app.put('/data', handler); // Fix app.options('*', cors()); // handle preflight for all routes app.put('/data', cors(), handler); ``` The preflight request hangs and the client sees a 4xx error. Easy to miss in development when using a proxy. **Assuming GET is always simple** Adding any custom header like `X-Request-ID` turns a GET into a non-simple request, triggering preflight. The request still works, just slower. Where custom headers are not needed, do not add them. **Dev proxy that hides the real problem** Create React App's `"proxy"` setting in `package.json` bypasses CORS in development. The app deploys to Vercel or Netlify, and everything breaks. Configure server CORS from day one. ### Real-world usage - **Express**: `app.use(cors({ origin: 'https://myapp.com' }))` in REST API servers - **Next.js**: set headers via `NextResponse` or middleware - **AWS API Gateway**: enable CORS per endpoint in the console - **Cloudflare Workers**: `response.headers.set('Access-Control-Allow-Origin', '*')` - **Create React App (dev only)**: `"proxy": "http://localhost:3001"` in `package.json` ### Follow-up questions **Q:** What is the same-origin policy? **A:** A browser rule that blocks cross-origin reads when scheme, host, or port differ. Tags like `<img>` and `<script>` can load from any origin, but fetch and XMLHttpRequest cannot read cross-origin responses without CORS headers. **Q:** What is the difference between a simple request and a preflight request? **A:** Simple requests use GET, POST, or HEAD with standard headers and no `application/json` Content-Type. Everything else triggers an `OPTIONS` preflight before the actual request. **Q:** Why does CORS not apply to Node.js scripts? **A:** CORS is enforced only by browsers. Node.js has no same-origin policy, so it ignores CORS headers entirely. Any server-to-server HTTP request skips CORS. **Q:** Can you configure CORS without a library? **A:** Yes. Set `res.header('Access-Control-Allow-Origin', origin)` in middleware and handle `OPTIONS` separately. The `cors` npm package just automates this pattern. **Q:** Why can `mode: 'no-cors'` still cause issues in a service worker? **A:** It returns an opaque response with no readable status or body. Caching opaque responses in a service worker can store error responses alongside successful ones with no way to tell them apart, which breaks offline behavior. ## Examples ### Express server with CORS (development setup) ```js // server.js - Express on port 3001 const express = require('express'); const app = express(); app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', 'http://localhost:3000'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') return res.sendStatus(200); next(); }); app.get('/api/user', (req, res) => res.json({ name: 'Alex' })); app.listen(3001); ``` ```jsx // App.js - React on localhost:3000 import { useEffect, useState } from 'react'; function App() { const [user, setUser] = useState(null); useEffect(() => { fetch('http://localhost:3001/api/user') .then(r => r.json()) .then(setUser); }, []); return <div>{user?.name || 'Loading...'}</div>; // Output: Alex } ``` The server explicitly allows `http://localhost:3000`. Remove that header and the browser holds the response, JavaScript sees nothing, and the console shows a CORS error. ### Credentials and exact origin When a request includes cookies or an `Authorization` header, `*` stops working. ```js // server.js - credentials require an exact origin app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', 'http://localhost:3000'); // NOT * res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') return res.sendStatus(200); next(); }); ``` ```js // Client fetch('http://localhost:3001/api/private', { credentials: 'include', // sends cookies }) .then(r => r.json()) .then(console.log); // Works with exact origin + credentials: true // Fails with *: "The value of the 'Access-Control-Allow-Origin' header // must not be the wildcard '*' when the request's credentials mode is 'include'" ``` I have seen this break production deploys more than once. Someone enables `credentials: include` on the client and forgets to update the server from `*` to the actual origin. ### Preflight in detail Here is what the browser actually sends before a POST with JSON: ```js // This fetch triggers a preflight automatically: fetch('https://api.example.com/data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 1 }), }); // Preflight request the browser generates: // OPTIONS /data HTTP/1.1 // Origin: https://yourapp.com // Access-Control-Request-Method: POST // Access-Control-Request-Headers: Content-Type // Required server response: // Access-Control-Allow-Origin: https://yourapp.com // Access-Control-Allow-Methods: POST // Access-Control-Allow-Headers: Content-Type // Access-Control-Max-Age: 86400 <- cache the preflight result for 24 hours ``` `Access-Control-Max-Age` is worth knowing for interviews. It tells the browser to cache the preflight result for N seconds, so it does not fire an `OPTIONS` request on every call.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.