Skip to content

Sessions and Cookies in Node.js

Cookies and server-side sessions are the traditional way to keep a user logged in across requests. This lesson covers how sessions work in Express and how to configure cookies securely.

How Session-Based Authentication Works

After a successful login, the server creates a session, a small piece of server-side state, usually keyed by a randomly generated session ID, and sends that ID to the browser as an HTTP cookie. On every subsequent request, the browser automatically includes that cookie, and the server looks up the associated session to know who's making the request.

express-session handles the mechanics: generating session IDs, setting the cookie, and reading/writing session data against a configurable store (in-memory for development, Redis or a database for production).

import session from 'express-session';

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { secure: true, httpOnly: true, maxAge: 24 * 60 * 60 * 1000 },
}));

app.post('/login', async (req, res) => {
  const user = await authenticate(req.body);
  req.session.userId = user.id;
  res.json({ message: 'Logged in' });
});

app.get('/profile', (req, res) => {
  if (!req.session.userId) return res.status(401).json({ error: 'Not logged in' });
  res.json({ userId: req.session.userId });
});

req.session is automatically created and persisted per browser by express-session, setting req.session.userId is all it takes to "log the user in" for future requests.

Secure Cookie Options

cookie: {
  httpOnly: true,   // JavaScript on the page can't read the cookie
  secure: true,      // only sent over HTTPS
  sameSite: 'lax',    // limits cross-site cookie sending
  maxAge: 86400000,   // expiration in milliseconds
}
  • httpOnly: true prevents client-side JavaScript from reading the cookie, mitigating XSS-based theft.
  • secure: true ensures the cookie is only ever sent over HTTPS connections.
  • sameSite controls whether the cookie is sent on cross-site requests, reducing CSRF risk.
  • maxAge sets how long the cookie (and its session) remains valid before expiring.

Sessions & Cookies Cheatsheet

Configuration and concepts around session-based authentication.

Setting Purpose
secret Signs the session ID cookie to prevent tampering
store Where session data lives (memory, Redis, database)
cookie.httpOnly Blocks JavaScript access to the cookie
cookie.secure Requires HTTPS for the cookie to be sent
cookie.sameSite Limits cross-site cookie sending
req.session.destroy() Ends a session immediately (logout)

Why the Default Memory Store Isn't for Production

express-session's default in-memory store is fine for local development but unsuitable for production: it leaks memory over time, and it isn't shared across multiple server instances, so a user's session might only exist on the one server that created it.

import session from 'express-session';
import { RedisStore } from 'connect-redis';
import { createClient } from 'redis';

const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
}));

A shared store like Redis lets any server instance behind a load balancer look up the same session data.

Logging Out and Destroying Sessions

Logging out should fully destroy the server-side session, not just clear client-side state, otherwise the session ID cookie (if somehow reused) could still be valid.

app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: 'Logout failed' });
    res.clearCookie('connect.sid');
    res.json({ message: 'Logged out' });
  });
});

Common Mistakes

  • Using the default in-memory session store in production, causing memory leaks and inconsistent behavior across servers.
  • Forgetting httpOnly/secure cookie options, weakening protection against XSS and network interception.
  • Not calling req.session.destroy() on logout, leaving stale server-side session data behind.
  • Using a weak or hardcoded session secret instead of a long, random, environment-stored value.

Key Takeaways

  • Session-based auth stores a session ID in a cookie and looks up server-side state on each request.
  • express-session handles cookie management, but needs a production-grade store like Redis.
  • httpOnly, secure, and sameSite cookie options are essential for real-world security.
  • Logging out should destroy the server-side session, not just clear the cookie client-side.

Pro Tip

Set up a Redis-backed session store from the very start of a project meant for production, even during early development. Switching session stores later, once real users and sessions exist, is far more disruptive than starting with the right store from day one.