Files
What you'll learn
- Explain the same-origin policy and what CORS changes: a browser rule, lifted by the server's headers, that never stops a non-browser client
- Enable CORS on a Nest app for a list of origins, with the methods, headers, credentials and preflight cache the front end needs
- Read a preflight request and its answer, and tell it from the actual request
- Say why a refused origin still gets a 200 body and no Access-Control headers, and why '*' and credentials cannot be combined
CORS
The shelter's website lives at https://shelter.example; the API at https://api.shelter.example. A script on the website calls fetch('https://api.shelter.example/cats'), and the browser refuses to hand it the answer: blocked by CORS policy. Nothing is wrong with the API. The browser is enforcing the same-origin policy, its oldest rule: a page may read responses only from its own origin (scheme, host and port), because otherwise any page you visit could read your bank's API with your cookies attached. Cross-origin resource sharing is how a server lifts that rule for the origins it trusts, and it does so with headers, which makes it a server-side concern and a Nest concern.
What the browser does
Before showing a page a cross-origin response, the browser looks for Access-Control-Allow-Origin in it. If the header names the page's origin (or *), the page may read the body; if not, the page gets an error and no data. The request still happened and the server still answered; CORS never protected the server from anything, and it does nothing for a curl or another server, which send no Origin header and are never refused. It protects the user's browser from pages that would read things in their name.
For a request that could change something, the browser asks first. A preflight is an OPTIONS request with Origin, Access-Control-Request-Method and Access-Control-Request-Headers, sent before any POST, DELETE, or a request with a custom header such as Authorization. The server answers with what it allows, Access-Control-Allow-Methods and Access-Control-Allow-Headers, and only then does the real request go out. Access-Control-Max-Age lets the browser cache that answer, so the preflight is paid once, not per request. A plain GET with no custom headers is sent without preflight; the browser just checks the answer.
Credentials, cookies and the Authorization header, are not sent cross-origin unless the page asks (credentials: 'include') and the server agrees (Access-Control-Allow-Credentials: true). And a server that agrees may not answer Allow-Origin: *; it must name the origin, which is why the header echoes the request's origin rather than a pattern, and why the response says Vary: Origin, so that a cache does not serve one origin's answer to another.
Enabling it in Nest
Nest wraps Express's cors package. app.enableCors() with no arguments allows every origin, which is right for a public read-only API and wrong for one with accounts. The options are the package's:
app.enableCors({
origin: ['https://docs.example', /\.example\.org$/],
methods: ['GET', 'PUT'],
allowedHeaders: ['Content-Type', 'X-Requested-With'],
credentials: true,
maxAge: 3600,
});
origin may be a string, a list, a regular expression, or a function (origin, callback) that decides per request, for a list that lives in a database. methods and allowedHeaders fill the preflight answer; a header the front end sends that is not listed fails the preflight, and Authorization is the one people forget. The same options can be passed at creation, NestFactory.create(AppModule, { cors: options }), or { cors: true } for the permissive default.
Where it sits: CORS is middleware at the front of the pipeline, before guards, so its headers ride on every response, a 401 from the guard included. A page that could not read the status of an error would not know it was signed out.
Your task
The shelter's sites call the API; nobody else's may.
- In
main.ts, enable CORS forhttps://shelter.exampleand every subdomain ofshelter.example, forGET,POSTandDELETE, allowing theContent-TypeandAuthorizationheaders, with credentials, and a preflight cache of ten minutes.
Then, in the request panel, send GET /cats with an Origin header for https://evil.example and look at the answer: the body is there, and nothing in the headers lets a browser show it.
When it fails
GET /catsfrom the site has no Allow-Origin header:enableCorsis not called, or the origin list does not match (httpforhttps, a trailing slash, a pattern that requires a dot the apex domain does not have).- The preflight lists
GET,HEAD,PUT,PATCH,POST,DELETE:methodswas left at the default; the task narrows it. - The front end's request with a token fails at preflight:
Authorizationis missing fromallowedHeaders. - Cookies never arrive:
credentials: trueis missing on the server, or the page did not ask for them. Allow-Origin: *next toAllow-Credentials: true: the browser refuses that combination; name the origin.
Remember
- Same-origin is the browser's rule; CORS is the server's permission, given in headers and enforced by the browser only.
- A refused origin still gets the response; a curl gets no headers and needs none.
- Preflight:
OPTIONSwith the intended method and headers; the answer lists what is allowed and how long to remember it. - Credentials need
Allow-Credentials: trueand a named origin, never*;Vary: Originkeeps caches honest.
Stuck? Show a hint
main.ts: app.enableCors({ origin: ['https://shelter.example', /\.shelter\.example$/], methods: ['GET', 'POST', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, maxAge: 600 }) before listen.
Press Run tests to start the app. Its log appears here.Graded endpoints
The Origin header names an allowed site, so the answer carries Access-Control-Allow-Origin naming it back, Allow-Credentials, and Vary: Origin so a cache keeps answers per origin
The regular expression matches admin.shelter.example; the header echoes the exact origin, never the pattern
The server still answers 200 with the body; it just grants nothing, and it is the browser on evil.example that refuses to show the page the result
No Origin header, no CORS: a server-to-server call or a curl needs no permission and gets no headers
The browser asks first: may this origin POST here with these headers? An empty 204 lists the allowed methods and headers, and Max-Age lets the browser skip asking again for ten minutes
Answered, but with no permissions in it, so the browser never sends the real request
The real request after a preflight; a POST with a JSON body carries the same origin headers as a GET
Authorization was allowed in the preflight, so the browser sends it; the guard reads it as always
CORS headers ride on error responses too, or the page could not even read the status
Staff add a cat from the admin page
Public, and answered to anyone; the headers decide only what a browser may show
DELETE is in the allowed methods; a 204 carries the origin header like any response