InteractiveFrameworks

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.

  1. In main.ts, parse cookies on every request with a secret.
  2. In cats/cookies.decorator.ts, @Cookies() reads a plain cookie by name (or all of them), @SignedCookies() a signed one.
  3. POST /cats/theme remembers the theme in a plain cookie; POST /cats/:id/favourite remembers the cat in a signed, httpOnly cookie; DELETE /cats/favourite forgets 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/theme always says light after choosing dark: the cookie is set but never parsed; cookieParser() is missing, so request.cookies is undefined and the decorator returns nothing.
  • The favourite is set but GET /cats/favourite is 404 No favourite yet: the cookie was written without signed: true, so it lives in cookies, not signedCookies; or the middleware has no secret, in which case cookie-parser cannot sign at all and res.cookie throws cookieParser("secret") required for signed cookies.
  • request.signedCookies.favourite is false: 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: clearCookie was called on a different path than the one the cookie was set with, or maxAge expired it.

Remember

  • app.use(cookieParser(secret)) fills request.cookies and, for verified signed ones, request.signedCookies.
  • response.cookie(name, value, options) sets, clearCookie deletes; passthrough keeps the handler's return value as the body.
  • httpOnly hides a cookie from page scripts; secure, maxAge, path and sameSite are 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').