Skip to content

Intermediate · Connecting frontend and backend · topic 18 of 34 · 30 min

CORS, origins and cookies

Explanation

An origin is the scheme, host and port of a URL, so localhost:5173 and localhost:3000 are different origins. Browsers block a page from reading answers from another origin unless the server allows it with CORS headers. Sending cookies across origins needs credentials: "include" plus an exact allowed origin. The easiest fix is to avoid the problem: serve the app and the API from the same origin.

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:5173Same origin?Why
http://localhost:5173/notesYesOnly the path is different
http://localhost:3000/api/notesNoDifferent port
https://localhost:5173NoDifferent scheme
http://127.0.0.1:5173NoDifferent 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.

Think of it like this: Think of the security guard at a housing society gate. A delivery person from outside can ask for a parcel for flat 302, but the guard hands it over only if the resident has put that name on the visitor list. The browser is the guard, CORS headers are the visitor list, and the server (the resident) writes the list. Someone who climbs the back wall, like a script using curl, never meets the guard at all.

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:

HeaderMeaning
Access-Control-Allow-OriginThe origin allowed to read the answer, for example http://localhost:5173
Access-Control-Allow-Credentials: trueThe answer may be read even when cookies were sent
Access-Control-Allow-MethodsMethods allowed in a preflight answer, for example GET, POST, PATCH, DELETE
Access-Control-Allow-HeadersRequest headers allowed, for example Content-Type
Vary: OriginTells 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.

output
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: 600
A preflight and its answer. Max-Age lets the browser remember the answer for 600 seconds instead of asking before every request.

Cookies 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.

javascript
// 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 cors package checks the Origin against the allow list and adds the headers, including the answer to preflight requests.
Careful: Never answer Access-Control-Allow-Origin: * together with credentials (browsers reject it), and never copy whatever Origin arrives back into the header. That would let every website on the internet read your users' notes. Use a fixed allow list of exact origins.

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.

javascript
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: { '/api': 'http://localhost:3000' },
  },
});
Now fetch("/api/notes") from the React app reaches Express, with no CORS and with normal cookies.

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.

Tip: CORS is a browser rule, not server security. curl, Postman and other servers ignore it completely. Your API must still check the session and note ownership on every request, as module 6 shows.

Watch the lesson

CORS, origins and cookies · 1/7

Origin = scheme + host + port :5173 is not :3000

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)

Task 1 · Code · Javascript

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.

Task 2 · Code · Javascript

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 "*".

Task 3 · Practice

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

Question 1

1.Which pair is the SAME origin?

Question 2

2.The frontend sends fetch with credentials: "include". Which server answer lets the browser read the result?

Question 3

3.What is a preflight request?

Question 4

4.Why is CORS not a replacement for checking logins on the server?

Question 5

5.What is the simplest way to avoid CORS in development with Vite?

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.

← Previous: Calling the API with fetch