Difficulty: Intermediate
Session-based authentication is the traditional approach to managing user login state in web applications. When a user logs in, the server creates a session - a server-side data store associated with that user - and sends a session ID to the client as a cookie. On subsequent requests, the browser automatically includes this cookie, allowing the server to look up the session and identify the user.
The `express-session` middleware handles the complexity of session management in Express applications. It generates a unique session ID, creates a session object on the server, sets a cookie on the client, and deserializes the session on each request. The session object (req.session) acts like a per-user key-value store where you can save any data - user ID, role, preferences, shopping cart contents, and more.
By default, express-session stores sessions in memory (MemoryStore), which is fine for development but completely unsuitable for production. Memory stores do not scale across multiple server instances, leak memory over time, and lose all sessions when the server restarts. In production, you should use a dedicated session store like connect-redis (Redis), connect-mongo (MongoDB), or connect-pg-simple (PostgreSQL). Redis is the most popular choice due to its speed and built-in expiration support.
The session cookie contains only the session ID, not the actual session data. This is fundamentally different from JWTs, where the client holds all the data. Because the server holds the session data, the server can invalidate sessions instantly (e.g., on logout or password change) - something that is much harder with JWTs. However, this server-side storage means sessions require database lookups on every request and are harder to scale across multiple servers.
Choosing between sessions and JWTs depends on your architecture. Sessions are better when you need immediate revocation (security-critical apps), when you store large amounts of user state, or when you have a monolithic server. JWTs are better for stateless APIs, microservices architectures, and mobile applications where cookies are less convenient. Many production systems use a hybrid: JWTs for API authentication with short expiry, backed by server-side session tracking for refresh token management.
const express = require('express');
const session = require('express-session');
const app = express();
app.use(express.json());
app.use(session({
secret: 'my-session-secret',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000, // 24 hours
httpOnly: true,
secure: false // set true in production with HTTPS
}
}));
const users = [{ id: 1, email: 'alice@test.com', password: 'hashed_pw' }];
app.post('/login', (req, res) => {
const { email } = req.body;
const user = users.find(u => u.email === email);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
req.session.userId = user.id;
req.session.role = 'student';
console.log('Session created:', req.sessionID);
res.json({ message: 'Logged in' });
});
app.get('/profile', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
res.json({ userId: req.session.userId, role: req.session.role });
});
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' });
});
});
console.log('Session auth routes configured');
On login, user data is saved in req.session. The session ID is sent as a cookie. On subsequent requests, express-session deserializes the session using the cookie. On logout, the session is destroyed and the cookie is cleared.
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
async function setupApp() {
const redisClient = createClient({ url: 'redis://localhost:6379' });
await redisClient.connect();
console.log('Redis connected for sessions');
const app = express();
app.use(express.json());
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET || 'fallback-secret',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax'
}
}));
// Session data now persists in Redis
// Survives server restarts
// Shared across multiple server instances
console.log('Redis session store configured');
return app;
}
console.log('Redis store setup function defined');
connect-redis stores session data in Redis instead of memory. This is production-ready: sessions persist across server restarts, can be shared across multiple app instances behind a load balancer, and Redis automatically handles expiration.
express-session, session stores, cookies, session ID, session vs JWT, connect-redis