InteractiveFrameworks

Session

Keep each visitor's data on the server behind a signed session cookie: count visits, remember the cats they looked at, and forget them on request, with express-session and @Session().

What you'll learn

  • Register express-session with a secret and explain resave, saveUninitialized and the memory store's limits
  • Read and write a visitor's session from a handler with @Session(), and know when the cookie is set
  • Destroy a session, and place session middleware in the request pipeline relative to guards

A cookie holds one value, and the visitor holds the cookie. As soon as an app wants to remember more than a preference, a visit count, the last cats someone looked at, a shopping basket, a logged-in user, stuffing it all into cookies becomes both awkward and unsafe. A session turns the problem around: the server keeps the visitor's data, and the cookie holds only an id that points at it. The visitor cannot read or change what the server stores, and the app reads and writes a plain object. This lesson adds sessions to the cats API with the Express middleware the docs use.

express-session

Like cookie-parser, it is middleware registered once:

import session from 'express-session';

app.use(
  session({
    secret: 'my-secret',
    resave: false,
    saveUninitialized: false,
  }),
);

secret signs the session id cookie, so an id cannot be forged; pass an array to rotate secrets. resave: false skips writing a session that did not change, saveUninitialized: false skips creating one for a visitor who stored nothing, which keeps the store small and matters for consent rules. The cookie is named connect.sid; cookie: { maxAge, secure, httpOnly, sameSite } sets its rules, and the docs recommend secure: true in production, behind HTTPS.

The middleware stores sessions in memory by default. The docs are blunt about it: the MemoryStore "will leak memory under most conditions", is not shared between processes, and is for development only. Production hands store a Redis or database-backed store from the list on the project's page. In this runtime a run is one process that dies with the tab, so memory is exactly right.

Reading and writing

From then on every request carries request.session, an object that persists between requests from the same visitor:

@Get()
findAll(@Req() request: Request) {
  request.session.visits = request.session.visits ? request.session.visits + 1 : 1;
}

Nest has a decorator for it, so the handler need not take the whole request:

@Get()
findAll(@Session() session: Record<string, any>) {
  session.visits = session.visits ? session.visits + 1 : 1;
}

Assigning a property is enough; the middleware saves the session when the response ends and sets the cookie on the first response that stored something. The object also carries methods: session.destroy(callback) deletes it and its cookie, session.regenerate(callback) gives it a new id (do this at login, so an id handed out before authentication cannot be reused), session.save(callback) writes it explicitly. They take Node-style callbacks; wrap one in a promise to await it.

Where it sits

Session middleware runs before guards, like all middleware: middleware → guards → interceptors → pipes → handler. That is the point of it for authentication, which the Security course builds on this lesson: a guard reads request.session.user and decides before the handler runs.

Your task

The cats API remembers each visitor.

  1. In main.ts, give every request a session, signed with a secret, saving neither unmodified nor uninitialised sessions.
  2. GET /cats/visits counts the visitor's visits.
  3. GET /cats/:id records the cat at the front of the visitor's recent list, with no duplicates and three entries at most; GET /cats/recent returns those cats, most recent first.
  4. POST /cats/forget destroys the session; the next visit starts from nothing.

Run the starter and read GET /cats/visits: the handler asks for @Session() and gets undefined, because nothing gave the request a session. Then look at the first response after you add the middleware: no Set-Cookie, because nothing was stored; and at the first that stores something.

When it fails

  • TypeError: Cannot read properties of undefined (reading 'visits'): @Session() handed over undefined; the middleware is not registered, or is registered after the routes.
  • Every request counts as the first visit: the session cookie is not coming back. In a browser, a secure: true cookie is dropped over plain HTTP; here, check that the first response that stored something carried Set-Cookie: connect.sid=....
  • session.destroy is not a function: the parameter is typed as a plain object; the methods exist at runtime, declare them in the type you use.
  • The recent list keeps growing or repeats a cat: the list is rebuilt from the previous one; filter the cat out before putting it at the front, and slice to three.

Remember

  • app.use(session({ secret, resave: false, saveUninitialized: false })): the cookie holds an id, the server holds the data.
  • @Session() hands a handler the session object; assigning a property persists it.
  • destroy, regenerate and save take callbacks; wrap them to await.
  • The default memory store is for development; production uses a shared store and secure cookies.
Stuck? Show a hint

main.ts: import session from 'express-session'; app.use(session({ secret: 'shelter-secret', resave: false, saveUninitialized: false })). visits: session.visits = session.visits ? session.visits + 1 : 1. findOne: session.recent = [cat.id, ...(session.recent ?? []).filter((seen) => seen !== cat.id)].slice(0, 3). recent: this.catsService.findMany(session.recent ?? []). forget: await new Promise((resolve, reject) => session.destroy((err) => (err ? reject(err) : resolve()))).