Cookies
Remember a visitor between requests: parse cookies with cookie-parser, read them through custom decorators, set a plain cookie for a theme and a signed one for a favourite cat that nobody can forge.
What you'll learn
- Register cookie-parser with a secret and read cookies from the request through a custom parameter decorator
- Set and clear cookies from a handler with @Res({ passthrough: true }), choosing httpOnly and the other options deliberately
- Explain what a signed cookie guarantees, and read one from signedCookies rather than cookies
HTTP forgets. Each request arrives on its own, and the server has no idea that the visitor asking for a cat is the one who chose a dark theme a minute ago. A cookie is the mechanism the browser offers to remember: a small named value the server hands out in a response header, which the browser stores and sends back, unasked, with every later request to the same site. The cats API will use two: one a visitor may set for themselves, and one that must be trusted, which is a different thing entirely.
Reading cookies
Nest itself does not parse the Cookie header. The Express middleware cookie-parser does, and it is registered the way any middleware is, once, before the app listens:
import cookieParser from 'cookie-parser';
app.use(cookieParser());
From then on request.cookies is an object, { theme: 'dark' }, on every request. A handler reads it from @Req():
@Get()
findAll(@Req() request: Request) {
console.log(request.cookies);
}
Reaching for the whole request to read one cookie is clumsy, and Basics lesson 10 taught the fix: a custom parameter decorator. The docs' version takes a name:
export const Cookies = createParamDecorator((data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return data ? request.cookies?.[data] : request.cookies;
});
@Get()
findAll(@Cookies('name') name: string) {}
The optional chaining matters: on a request with no cookies, or in an app that forgot the middleware, request.cookies is undefined and the decorator returns undefined rather than throwing.
Setting cookies
A cookie is set on the response, and with passthrough the handler still returns its body as usual:
@Get()
findAll(@Res({ passthrough: true }) response: Response) {
response.cookie('key', 'value');
}
response.cookie(name, value, options) writes a Set-Cookie header. The options are the cookie's rules: httpOnly: true hides it from JavaScript in the page, secure: true sends it over HTTPS only, maxAge in milliseconds or expires as a date give it a lifetime (without either it dies with the browser session), path and domain scope it, sameSite controls cross-site requests. response.clearCookie(name) sets it again with a date in the past, which is how a cookie is deleted.
Signed cookies: trusting what comes back
A cookie is stored on the visitor's machine, and a visitor can edit it. For a theme that is fine. For "the cat you own", "the account you are logged into", or anything the server acts on, it is not. Signed cookies solve this without hiding the value: the server appends a signature computed from the value and a secret only it knows. The browser sends value and signature back; cookie-parser recomputes the signature and, if it matches, puts the cookie in request.signedCookies. A cookie whose signature does not match, because the value was changed, appears there as false, and a plain cookie never appears there at all.
Two changes make it work: the middleware gets the secret, cookieParser('a long random secret'), and the cookie is written with signed: true, which prefixes the value with s: and the signature. A signed cookie is not encrypted; the value is readable. What it cannot be is altered.
Your task
The cats API remembers a visitor's theme and their favourite cat.
- In
main.ts, parse cookies on every request with a secret. - In
cats/cookies.decorator.ts,@Cookies()reads a plain cookie by name (or all of them),@SignedCookies()a signed one. POST /cats/themeremembers the theme in a plain cookie;POST /cats/:id/favouriteremembers the cat in a signed,httpOnlycookie;DELETE /cats/favouriteforgets it.
The request panel keeps the app's cookies between requests, like a browser: set the favourite, then read it. Then edit the cookie's value in the panel by hand and read again, and watch a tampered signed cookie become no favourite at all.
When it fails
GET /cats/themealways sayslightafter choosingdark: the cookie is set but never parsed;cookieParser()is missing, sorequest.cookiesis undefined and the decorator returns nothing.- The favourite is set but
GET /cats/favouriteis404 No favourite yet: the cookie was written withoutsigned: true, so it lives incookies, notsignedCookies; or the middleware has no secret, in which casecookie-parsercannot sign at all andres.cookiethrowscookieParser("secret") required for signed cookies. request.signedCookies.favouriteisfalse: the signature did not verify; the value was changed on the client, or the secret changed on the server.- The cookie does not come back at all:
clearCookiewas called on a different path than the one the cookie was set with, ormaxAgeexpired it.
Remember
app.use(cookieParser(secret))fillsrequest.cookiesand, for verified signed ones,request.signedCookies.response.cookie(name, value, options)sets,clearCookiedeletes;passthroughkeeps the handler's return value as the body.httpOnlyhides a cookie from page scripts;secure,maxAge,pathandsameSiteare its rules.- A signed cookie can be read but not altered; anything the server acts on is signed.
Stuck? Show a hint
main.ts: import cookieParser from 'cookie-parser'; app.use(cookieParser('shelter-secret')). Decorators: ctx.switchToHttp().getRequest() then request.cookies?.[data] (or the whole object), and request.signedCookies for the signed one. Controller: res.cookie('theme', body.theme); res.cookie('favourite', String(cat.id), { signed: true, httpOnly: true }); res.clearCookie('favourite').