Cookies and Sessions

Cookies and sessions let your server remember things about a user between requests. HTTP is stateless by design — each request arrives with no memory of previous ones. Cookies and sessions solve this by storing data either on the client (cookies) or on the server (sessions). They power login systems, shopping carts, user preferences, and personalized experiences across the web.

The Library Membership Card Analogy

A cookie is like a membership card the library gives you. You carry it in your wallet, and every time you visit, you show it at the desk. The card holds your member number. A session is like the library's filing system — when you show your card number, the librarian looks up your full borrowing history in their internal system. The card carries only an ID; the real data stays with the library.

COOKIE approach:
  Server → sends data to browser → browser stores it → browser sends it back every request
  All data lives on the client

SESSION approach:
  Server → sends only a session ID → stores data server-side → browser sends ID back
  ID on client, data on server

┌────────────────────────────────────────────────────────────┐
│              Cookie vs Session Storage                     │
├─────────────────────┬──────────────────────────────────────┤
│ Cookie              │ Session                              │
├─────────────────────┼──────────────────────────────────────┤
│ Data in browser     │ Data on server                       │
│ Visible to client   │ Hidden from client (only ID visible) │
│ Limit ~4KB          │ No practical data size limit         │
│ No server storage   │ Requires server-side store           │
│ Works across tabs   │ Works across requests                │
└─────────────────────┴──────────────────────────────────────┘

Working with Cookies

Install cookie-parser

npm install cookie-parser
const express = require('express');
const cookieParser = require('cookie-parser');

const app = express();
app.use(cookieParser()); // Parses Cookie header into req.cookies

Setting a Cookie

app.get('/set-cookie', (req, res) => {
  res.cookie('username', 'alice', {
    httpOnly: true,                      // JS cannot read this cookie
    secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
    sameSite: 'strict',                  // Blocks cross-site sending
    maxAge: 7 * 24 * 60 * 60 * 1000     // Expires in 7 days (ms)
  });

  res.json({ message: 'Cookie set!' });
});

Reading a Cookie

app.get('/read-cookie', (req, res) => {
  const username = req.cookies.username;

  if (!username) {
    return res.status(400).json({ error: 'No cookie found' });
  }

  res.json({ username });
});

Deleting a Cookie

app.get('/clear-cookie', (req, res) => {
  res.clearCookie('username');
  res.json({ message: 'Cookie cleared' });
});

Signed Cookies (Tamper Detection)

Signed cookies include a cryptographic signature. Any modification to the cookie value makes the signature invalid, preventing users from altering the data:

app.use(cookieParser('my-secret-signing-key'));

// Set a signed cookie
res.cookie('userId', '42', { signed: true });

// Read a signed cookie
app.get('/profile', (req, res) => {
  const userId = req.signedCookies.userId; // Only valid if signature matches
  if (!userId) {
    return res.status(401).json({ error: 'Invalid or missing cookie' });
  }
  res.json({ userId });
});

Cookie Options Reference

┌──────────────────────────────────────────────────────────────────┐
│                    Cookie Options                                │
├──────────────────┬───────────────────────────────────────────────┤
│ httpOnly         │ Blocks JavaScript access (prevents XSS theft) │
│ secure           │ Sends only over HTTPS                         │
│ sameSite         │ 'strict' | 'lax' | 'none' (CSRF protection)   │
│ maxAge           │ Lifetime in milliseconds                      │
│ expires          │ Specific Date object for expiry               │
│ domain           │ Which domains receive this cookie             │
│ path             │ Which URL paths receive this cookie           │
│ signed           │ Adds signature for tamper detection           │
└──────────────────┴───────────────────────────────────────────────┘

Working with Sessions

Sessions store data on the server and give the browser only a session ID cookie. The server looks up the ID on every request to retrieve the stored data.

Install express-session

npm install express-session
const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET || 'keyboard-cat',
  resave: false,            // Don't save unchanged sessions
  saveUninitialized: false, // Don't save empty sessions
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 24 * 60 * 60 * 1000 // 1 day
  }
}));

Using Sessions for Login

// Login: create session
app.post('/login', (req, res) => {
  const { username, password } = req.body;

  // Verify credentials (simplified — use bcrypt in production)
  if (username === 'alice' && password === 'secret') {
    req.session.userId = 42;
    req.session.username = 'alice';
    req.session.role = 'user';
    return res.json({ message: 'Logged in successfully' });
  }

  res.status(401).json({ error: 'Invalid credentials' });
});

// Protected route: check session
app.get('/dashboard', (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Please log in first' });
  }

  res.json({ message: `Welcome ${req.session.username}!` });
});

// Logout: destroy session
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ error: 'Could not log out' });
    }
    res.clearCookie('connect.sid'); // Clear the session ID cookie
    res.json({ message: 'Logged out successfully' });
  });
});

Session Flow Diagram

User POSTs /login with credentials
            │
            ▼
  Server verifies credentials
            │
  Express creates session:
  ┌─────────────────────────────────────────┐
  │  Session ID: abc123xyz                  │
  │  Data: { userId: 42, username: 'alice' }│
  │  Stored in: server memory / database    │
  └──────────────────┬──────────────────────┘
                     │
  Set-Cookie: connect.sid=abc123xyz
                     │
                     ▼
  Browser stores the cookie
                     │
  Next request: GET /dashboard
  Cookie: connect.sid=abc123xyz
                     │
                     ▼
  Server looks up session by ID
  Finds { userId: 42, username: 'alice' }
  Attaches to req.session
                     │
                     ▼
  Route handler reads req.session.username

Persistent Session Store with MongoDB

By default, sessions live in server memory and disappear on restart. Use a database-backed store for production:

npm install connect-mongo
const MongoStore = require('connect-mongo');

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  store: MongoStore.create({
    mongoUrl: process.env.MONGODB_URI,
    ttl: 24 * 60 * 60 // Session expires after 1 day (seconds)
  }),
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 24 * 60 * 60 * 1000
  }
}));

Sessions now survive server restarts because they live in MongoDB, not in memory.

Session Middleware for Protected Routes

const requireAuth = (req, res, next) => {
  if (!req.session || !req.session.userId) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  next();
};

// Apply to protected routes
app.get('/account', requireAuth, (req, res) => {
  res.json({ userId: req.session.userId, name: req.session.username });
});

app.get('/orders', requireAuth, (req, res) => {
  res.json({ message: `Orders for user ${req.session.userId}` });
});

Cookies vs Sessions vs JWT: Choosing the Right Tool

┌──────────────────────────────────────────────────────────────────┐
│             When to Use Each Authentication Method               │
├─────────────────────┬────────────────────────────────────────────┤
│ Cookies             │ Store non-sensitive preferences (theme,    │
│ (data in browser)   │ language). Small data only.                │
├─────────────────────┼────────────────────────────────────────────┤
│ Sessions            │ Traditional web apps with server-side      │
│ (ID in browser,     │ rendering. User must log out explicitly.   │
│  data on server)    │ Easy to invalidate server-side.            │
├─────────────────────┼────────────────────────────────────────────┤
│ JWT                 │ APIs consumed by mobile apps or single-    │
│ (token in browser)  │ page apps. Stateless — no server storage.  │
└─────────────────────┴────────────────────────────────────────────┘

Summary

Cookies store data directly in the browser and travel automatically with every request to the same domain. Use cookie-parser to read cookies in Express, res.cookie() to set them, and res.clearCookie() to remove them. Always set httpOnly and secure on authentication cookies. Sessions use a small session ID cookie while storing the actual data on the server. Use express-session to create and manage sessions, store user data in req.session, and destroy sessions on logout. In production, replace the default in-memory session store with a persistent store like connect-mongo so sessions survive server restarts.

Leave a Comment

Your email address will not be published. Required fields are marked *