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.
