Simple session middleware for Express.js applications
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Session store interface and implementations for persisting session data beyond memory, including the built-in MemoryStore and requirements for custom store implementations.
Abstract base class that all session stores must extend, providing common functionality and event handling.
/**
* Abstract base class for session stores
* Extends EventEmitter for connection status events
*/
class Store extends EventEmitter {
/**
* Regenerate session by destroying old one and creating new one
* @param req - Express request object
* @param callback - Callback function
*/
regenerate(req: Request, callback: (err?: Error) => void): void;
/**
* Load session by ID and create Session instance
* @param sid - Session ID
* @param callback - Callback with error and session
*/
load(sid: string, callback: (err?: Error, session?: Session) => void): void;
/**
* Create Session instance from session data
* @param req - Express request object
* @param sess - Session data
* @returns Session instance
*/
createSession(req: Request, sess: any): Session;
}Usage Examples:
const { Store } = require('express-session');
const EventEmitter = require('events');
// Custom store extending base Store class
class CustomStore extends Store {
constructor(options = {}) {
super();
this.options = options;
this.sessions = new Map();
}
// Implement required methods
destroy(sid, callback) {
this.sessions.delete(sid);
if (callback) callback();
}
get(sid, callback) {
const session = this.sessions.get(sid);
if (callback) callback(null, session);
}
set(sid, session, callback) {
this.sessions.set(sid, session);
if (callback) callback();
}
}Methods that every session store implementation must provide.
interface RequiredStoreMethods {
/**
* Destroy/delete session by session ID
* @param sid - Session ID to destroy
* @param callback - Callback function called when complete
*/
destroy(sid: string, callback: (err?: Error) => void): void;
/**
* Get session data by session ID
* @param sid - Session ID to retrieve
* @param callback - Callback with error and session data
*/
get(sid: string, callback: (err?: Error, session?: any) => void): void;
/**
* Set/update session data by session ID
* @param sid - Session ID to store
* @param session - Session data to store
* @param callback - Callback function called when complete
*/
set(sid: string, session: any, callback: (err?: Error) => void): void;
}Usage Examples:
// Redis store implementation example
class RedisStore extends Store {
constructor(client) {
super();
this.client = client;
}
destroy(sid, callback) {
this.client.del(`session:${sid}`, callback);
}
get(sid, callback) {
this.client.get(`session:${sid}`, (err, data) => {
if (err) return callback(err);
try {
const session = data ? JSON.parse(data) : null;
callback(null, session);
} catch (parseErr) {
callback(parseErr);
}
});
}
set(sid, session, callback) {
const data = JSON.stringify(session);
const ttl = session.cookie && session.cookie.maxAge
? Math.floor(session.cookie.maxAge / 1000)
: 86400; // 24 hours default
this.client.setex(`session:${sid}`, ttl, data, callback);
}
}Methods that stores should implement for optimal functionality.
interface RecommendedStoreMethods {
/**
* Touch session to update expiration without changing data
* @param sid - Session ID to touch
* @param session - Current session data
* @param callback - Callback function called when complete
*/
touch?(sid: string, session: any, callback: (err?: Error) => void): void;
}Usage Examples:
// Store with touch method implementation
class DatabaseStore extends Store {
// ... required methods ...
touch(sid, session, callback) {
const expiry = new Date(Date.now() + (session.cookie.maxAge || 86400000));
this.db.query(
'UPDATE sessions SET expires = ? WHERE session_id = ?',
[expiry, sid],
callback
);
}
}
// Using touch functionality
app.use(session({
store: new DatabaseStore(),
resave: false, // Can set to false since store implements touch
saveUninitialized: false
}));Additional methods that stores may implement for enhanced functionality.
interface OptionalStoreMethods {
/**
* Get all sessions in the store
* @param callback - Callback with error and sessions object
*/
all?(callback: (err?: Error, sessions?: { [sid: string]: any }) => void): void;
/**
* Clear all sessions from the store
* @param callback - Callback function called when complete
*/
clear?(callback: (err?: Error) => void): void;
/**
* Get the count of sessions in the store
* @param callback - Callback with error and session count
*/
length?(callback: (err?: Error, count?: number) => void): void;
}Usage Examples:
// Admin endpoints using optional store methods
app.get('/admin/sessions', (req, res) => {
if (req.sessionStore.all) {
req.sessionStore.all((err, sessions) => {
if (err) return res.status(500).json({ error: 'Failed to get sessions' });
const sessionList = Object.keys(sessions).map(sid => ({
id: sid,
data: sessions[sid],
lastAccess: sessions[sid].lastAccess
}));
res.json({ sessions: sessionList });
});
} else {
res.status(501).json({ error: 'Store does not support listing sessions' });
}
});
app.delete('/admin/sessions', (req, res) => {
if (req.sessionStore.clear) {
req.sessionStore.clear((err) => {
if (err) return res.status(500).json({ error: 'Failed to clear sessions' });
res.json({ message: 'All sessions cleared' });
});
} else {
res.status(501).json({ error: 'Store does not support clearing sessions' });
}
});
app.get('/admin/session-count', (req, res) => {
if (req.sessionStore.length) {
req.sessionStore.length((err, count) => {
if (err) return res.status(500).json({ error: 'Failed to get session count' });
res.json({ count });
});
} else {
res.status(501).json({ error: 'Store does not support session counting' });
}
});Built-in memory-based session store for development use.
/**
* In-memory session store (development only)
* Not suitable for production due to memory leaks and scalability issues
*/
class MemoryStore extends Store {
constructor();
/** Internal session storage object */
sessions: { [sid: string]: string };
/** Get all active sessions */
all(callback: (err?: Error, sessions?: { [sid: string]: any }) => void): void;
/** Clear all sessions */
clear(callback: (err?: Error) => void): void;
/** Destroy session by ID */
destroy(sid: string, callback: (err?: Error) => void): void;
/** Get session by ID */
get(sid: string, callback: (err?: Error, session?: any) => void): void;
/** Set session by ID */
set(sid: string, session: any, callback: (err?: Error) => void): void;
/** Get session count */
length(callback: (err?: Error, count?: number) => void): void;
/** Touch session to update expiration */
touch(sid: string, session: any, callback: (err?: Error) => void): void;
}Usage Examples:
const { MemoryStore } = require('express-session');
// Explicit MemoryStore usage (development only)
const store = new MemoryStore();
app.use(session({
store: store,
secret: 'dev-secret',
resave: false,
saveUninitialized: false
}));
// Development session management
if (process.env.NODE_ENV === 'development') {
app.get('/dev/sessions', (req, res) => {
store.all((err, sessions) => {
res.json({
count: Object.keys(sessions).length,
sessions: sessions
});
});
});
app.delete('/dev/sessions', (req, res) => {
store.clear(() => {
res.json({ message: 'All sessions cleared' });
});
});
}Session stores emit events for connection status and error handling.
interface StoreEvents {
/** Emitted when store becomes unavailable */
'disconnect': () => void;
/** Emitted when store becomes available */
'connect': () => void;
/** Emitted on store errors */
'error': (error: Error) => void;
}Usage Examples:
// Store event handling
const store = new RedisStore(redisClient);
store.on('connect', () => {
console.log('Session store connected');
});
store.on('disconnect', () => {
console.log('Session store disconnected');
});
store.on('error', (err) => {
console.error('Session store error:', err);
});
// Graceful degradation
app.use(session({
store: store,
secret: 'secret',
resave: false,
saveUninitialized: false
}));
// Monitor store health
app.get('/health', (req, res) => {
// Store readiness is tracked internally by express-session
res.json({
status: 'ok',
sessionStore: 'connected' // Sessions will work normally
});
});Complete example of implementing a custom database store.
/**
* Example custom database store implementation
*/
class DatabaseStore extends Store {
constructor(database, options = {}) {
super();
this.db = database;
this.options = {
tableName: 'sessions',
ttl: 86400, // 24 hours default TTL
...options
};
}
}Usage Examples:
const mysql = require('mysql2');
class MySQLStore extends Store {
constructor(connection, options = {}) {
super();
this.db = connection;
this.tableName = options.tableName || 'sessions';
// Create sessions table if it doesn't exist
this.createTable();
}
createTable() {
const sql = `
CREATE TABLE IF NOT EXISTS ${this.tableName} (
session_id VARCHAR(128) PRIMARY KEY,
expires DATETIME,
data TEXT
)
`;
this.db.execute(sql);
}
destroy(sid, callback) {
const sql = `DELETE FROM ${this.tableName} WHERE session_id = ?`;
this.db.execute(sql, [sid], callback);
}
get(sid, callback) {
const sql = `
SELECT data FROM ${this.tableName}
WHERE session_id = ? AND expires > NOW()
`;
this.db.execute(sql, [sid], (err, results) => {
if (err) return callback(err);
try {
const session = results.length > 0
? JSON.parse(results[0].data)
: null;
callback(null, session);
} catch (parseErr) {
callback(parseErr);
}
});
}
set(sid, session, callback) {
const expires = new Date(Date.now() + (session.cookie.maxAge || 86400000));
const data = JSON.stringify(session);
const sql = `
INSERT INTO ${this.tableName} (session_id, expires, data)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE expires = VALUES(expires), data = VALUES(data)
`;
this.db.execute(sql, [sid, expires, data], callback);
}
touch(sid, session, callback) {
const expires = new Date(Date.now() + (session.cookie.maxAge || 86400000));
const sql = `UPDATE ${this.tableName} SET expires = ? WHERE session_id = ?`;
this.db.execute(sql, [expires, sid], callback);
}
clear(callback) {
const sql = `DELETE FROM ${this.tableName}`;
this.db.execute(sql, callback);
}
length(callback) {
const sql = `SELECT COUNT(*) as count FROM ${this.tableName} WHERE expires > NOW()`;
this.db.execute(sql, (err, results) => {
if (err) return callback(err);
callback(null, results[0].count);
});
}
}
// Usage
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'myapp'
});
const store = new MySQLStore(connection);
app.use(session({
store: store,
secret: 'my-secret',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24 // 24 hours
}
}));Best practices for error handling in session store implementations.
interface StoreErrorHandling {
/** Handle store connection errors gracefully */
handleConnectionError(error: Error): void;
/** Handle data corruption or parsing errors */
handleDataError(error: Error, sid: string): void;
}Usage Examples:
class RobustStore extends Store {
constructor(options) {
super();
this.options = options;
this.setupErrorHandling();
}
setupErrorHandling() {
this.on('error', (err) => {
console.error('Store error:', err);
// Emit disconnect event to disable sessions temporarily
this.emit('disconnect');
// Attempt reconnection
setTimeout(() => this.reconnect(), 5000);
});
}
get(sid, callback) {
try {
// Store operation
this.performGet(sid, (err, session) => {
if (err) {
// Log error but don't crash the application
console.error(`Session get error for ${sid}:`, err);
// Treat as session not found rather than error
return callback(null, null);
}
callback(null, session);
});
} catch (syncErr) {
console.error('Synchronous store error:', syncErr);
callback(null, null); // Graceful degradation
}
}
set(sid, session, callback) {
try {
this.performSet(sid, session, (err) => {
if (err) {
console.error(`Session set error for ${sid}:`, err);
// Continue without throwing - session will be lost but app continues
}
if (callback) callback();
});
} catch (syncErr) {
console.error('Synchronous store error:', syncErr);
if (callback) callback();
}
}
}