Intermediate · Connecting frontend and backend · topic 18 of 34 · 30 min
CORS, origins and cookies
Explanation
What is an origin?
An origin is three parts of a URL together: the scheme (http or https), the host (like localhost or notes.example.com) and the port (like 5173 or 3000). If any one of the three is different, it is a different origin. The path, such as /api/notes, does not matter.
| Compared with http://localhost:5173 | Same origin? | Why |
|---|---|---|
| http://localhost:5173/notes | Yes | Only the path is different |
| http://localhost:3000/api/notes | No | Different port |
| https://localhost:5173 | No | Different scheme |
| http://127.0.0.1:5173 | No | Different host name, even though it is the same computer |
The same-origin policy
The same-origin policy is a browser safety rule: JavaScript on a page may send a request to another origin, but it may not read the answer unless that server says it is allowed. Without this rule, any website you open could quietly call your email or bank site with your cookies and read your data. In development our Vite frontend runs on localhost:5173 and Express on localhost:3000, so every API call is cross-origin and is blocked by default.
CORS (Cross-Origin Resource Sharing) is the way a server tells the browser which other origins may read its answers. It does this with response headers:
| Header | Meaning |
|---|---|
| Access-Control-Allow-Origin | The origin allowed to read the answer, for example http://localhost:5173 |
| Access-Control-Allow-Credentials: true | The answer may be read even when cookies were sent |
| Access-Control-Allow-Methods | Methods allowed in a preflight answer, for example GET, POST, PATCH, DELETE |
| Access-Control-Allow-Headers | Request headers allowed, for example Content-Type |
| Vary: Origin | Tells caches the answer depends on the Origin, so one site's answer is not reused for another |
Preflight requests
For "non-simple" requests, such as PATCH or DELETE, or a POST with Content-Type: application/json, the browser first sends a preflight: an OPTIONS request that asks "may I send this?". Only if the server answers with the right Allow headers does the browser send the real request. You will see these OPTIONS requests in the Network tab of the browser DevTools.
OPTIONS /api/notes/7
Origin: http://localhost:5173
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: content-type
204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 600Cookies across origins
By default, fetch to another origin does not send or save cookies. Our login uses a session cookie (module 6 explains it fully), so the frontend must ask with credentials: "include", and the server must answer with Access-Control-Allow-Credentials: true and the exact origin, never *. The cookie's SameSite=Lax setting also matters: it allows the cookie between localhost:5173 and localhost:3000 (same site, because a site ignores the port), and between app.notes.in and api.notes.in, but not between two different domains such as a frontend on one hosting service and an API on another.
// server.js (Express), using the cors package
import cors from 'cors';
const allowList = ['http://localhost:5173', 'https://notes.example.com'];
app.use(cors({ origin: allowList, credentials: true }));
// frontend
await fetch('http://localhost:3000/api/notes', { credentials: 'include' });The easiest fix: one origin
If the browser only ever talks to one origin, CORS is not needed at all. In development, the Vite dev server can act as a proxy: a middleman that receives /api requests on localhost:5173 and forwards them to Express on localhost:3000. The browser thinks everything comes from 5173.
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: { '/api': 'http://localhost:3000' },
},
});In production, do the same with one domain: Express can serve the built React files and the API together, or a hosting rule can send /api/* to the backend and everything else to the frontend. With Next.js, the API routes already live on the same origin, and rewrites in next.config.js can forward /api to a separate backend.
Watch the lesson
CORS, origins and cookies · 1/7
Our React app runs on localhost 5173 and Express on localhost 3000. The browser treats these as two different origins.
Prefer a video by a teacher? Find free videos on “cors explained same origin policy preflight credentials” on YouTube ↗
In real life: A college results portal and its API
A college shows exam results at results.college.edu.in, while the API lives at api.college.edu.in. These are different origins, so the first version showed a blank page and a red CORS error in the console. The fix was a fixed allow list on the API with exactly https://results.college.edu.in, Allow-Credentials: true for the login cookie, and correct answers to preflight requests. Later the team moved the API under results.college.edu.in/api, and the CORS settings were no longer needed at all.
Practice (0/3 done)
Write isSameOrigin(a, b) that returns true when two full URLs have the same origin (scheme, host and port) and false otherwise. Tip: new URL(text).origin gives the origin and already removes default ports such as :443 for https.
Write corsHeaders(requestOrigin, allowList) for the Notes API. If requestOrigin is exactly one of the strings in allowList, return { "Access-Control-Allow-Origin": requestOrigin, "Access-Control-Allow-Credentials": "true", "Vary": "Origin" } (keys in this order). Otherwise (missing origin, unknown origin, look-alike origin) return an empty object {}. Never return "*".
Make the Notes app run on one origin in development. Add a Vite proxy so the React app on localhost:5173 can call fetch("/api/notes") and reach Express on localhost:3000. Then check it in the browser DevTools Network tab and write down what you see.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.