When you fill in a login form on a classic web app and the page just "remembers" you on every subsequent click, that's sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request.-based auth doing its job quietly in the background. It's the oldest pattern in web development, and it still fits a huge proportion of projects perfectly. This lesson breaks down how it works and shows you what a solid Express implementation looks like.
How sessions work
Think of a sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. like a coat-check ticket. When you arrive at a venue you hand over your coat (your credentials), and the attendant gives you a small numbered ticket (the session ID). Every time you want your coat back you show the ticket, you don't carry the coat around with you all evening.
The flow is:
- User sends their credentials (email + password).
- Server verifies them, then creates a session record containing the user's ID and any other data you need.
- Server stores the session in a session store (Redis, a database, or even a file in development).
- Server sends a cookieWhat is cookie?A small piece of data the browser stores and automatically sends with every request to the matching server, often used for sessions. to the browser containing only the session ID, nothing sensitive.
- On every subsequent request the browser sends the cookie automatically.
- Server reads the session ID from the cookie, fetches the matching record from the store, and knows who the user is.
The key detail: the cookie is opaque. It is just a random identifier. Your user data never leaves the server.
Setting up express-sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request.
npm install express-session
npm install connect-redis # or connect-pg-simple for PostgreSQLimport session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient();
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET, // Signs the cookie to prevent tampering
resave: false, // Don't re-save unchanged sessions
saveUninitialized: false, // Don't create a session until something is stored
name: 'sessionId', // Rename from the default 'connect.sid'
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
httpOnly: true, // JavaScript cannot read this cookie
maxAge: 24 * 60 * 60 * 1000, // 24 hours in milliseconds
sameSite: 'strict' // Blocks cross-site request forgery
}
}));Two options deserve special attention. saveUninitialized: false prevents the server from creating a session record for every anonymous visitor, important for privacy regulations and for keeping your session store lean. httpOnly: true means that even if an attacker injects malicious JavaScript into your page, that script cannot read the session cookieWhat is cookie?A small piece of data the browser stores and automatically sends with every request to the matching server, often used for sessions.. Always set it.
sameSite: 'strict' blocks the cookie from being sent with cross-origin requests entirely. If your frontend and API live on different domains (a common SPA setup) you may need sameSite: 'none' paired with secure: true, but that opens CSRF exposure you then need to handle separately with a CSRF token.AuthenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. routes
// Login
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await db.findUserByEmail(email);
if (!user || !await verifyPassword(password, user.passwordHash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Attach data to the session - this triggers a save to the store
req.session.userId = user.id;
req.session.email = user.email;
res.json({ message: 'Login successful', user: { id: user.id, email: user.email } });
});
// Logout - destroy the server-side record, then clear the cookie
app.post('/auth/logout', (req, res) => {
req.session.destroy();
res.clearCookie('sessionId');
res.json({ message: 'Logout successful' });
});
// Protected route
app.get('/profile', requireAuth, async (req, res) => {
const user = await db.findUserById(req.session.userId);
res.json(user);
});Notice that logout calls req.session.destroy() on the server before clearing the cookieWhat is cookie?A small piece of data the browser stores and automatically sends with every request to the matching server, often used for sessions.. This matters: if you only clear the cookie on the client, someone who captured the sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. ID earlier could still use it. Destroying the record server-side invalidates the ID immediately, which is one of the biggest advantages sessions have over JWTs.
Auth middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it.
Every protected route needs to verify that a valid sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. exists before doing any real work. A simple middleware handles this in one place so you never have to repeat the check:
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Authentication required' });
}
next();
}Attach it to any route or router: app.get('/dashboard', requireAuth, handler). If the session is missing or expired, the user gets a 401 and the handler never runs.
Quick reference
| Option | Recommended value | Why |
|---|---|---|
secret | Long random string from env | Signs the cookie; never hardcode |
resave | false | Avoids unnecessary store writes |
saveUninitialized | false | Don't create sessions for anonymous visitors |
cookie.httpOnly | true | Prevents XSS from stealing the cookie |
cookie.secure | true in production | Sends cookie over HTTPS only |
cookie.sameSite | 'strict' (same-origin) or 'none' (cross-origin) | CSRF mitigation |
cookie.maxAge | 24h–7d depending on app | Sets session lifetime |