How Cookies and Sessions Work
HTTP is stateless — every request is independent. Cookies are the mechanism browsers use to maintain state. The server sends a Set-Cookie header, the browser stores it, and sends it back on every subsequent request via the Cookie header.
Sessions take this further: the cookie stores only a random ID. The actual session data (who is logged in, what's in the cart) lives server-side, looked up by that ID.
Parsing Cookies
// session.js
function parseCookies(req) {
const cookieHeader = req.headers.cookie || '';
const cookies = {};
cookieHeader.split(';').forEach(pair => {
const [key, ...vals] = pair.trim().split('=');
if (key) cookies[key.trim()] = decodeURIComponent(vals.join('='));
});
return cookies;
}
Setting Cookies
function setCookie(res, name, value, options = {}) {
const parts = [`${name}=${encodeURIComponent(value)}`];
if (options.maxAge) parts.push(`Max-Age=${options.maxAge}`);
if (options.path) parts.push(`Path=${options.path}`);
if (options.httpOnly) parts.push('HttpOnly');
if (options.secure) parts.push('Secure');
if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
res.setHeader('Set-Cookie', parts.join('; '));
}
In-Memory Session Store
const crypto = require('crypto');
// sessionId → { data, createdAt, expiresAt }
const sessions = new Map();
const SESSION_COOKIE = 'sid';
const SESSION_MAX_AGE = 60 * 60 * 24; // 24 hours in seconds
function createSession(res, data = {}) {
const sessionId = crypto.randomBytes(32).toString('hex');
const now = Date.now();
sessions.set(sessionId, {
data,
createdAt: now,
expiresAt: now + SESSION_MAX_AGE * 1000
});
setCookie(res, SESSION_COOKIE, sessionId, {
maxAge: SESSION_MAX_AGE,
httpOnly: true, // JS can't read it — XSS protection
sameSite: 'Lax', // CSRF protection
path: '/'
});
return sessionId;
}
function getSession(req) {
const cookies = parseCookies(req);
const sessionId = cookies[SESSION_COOKIE];
if (!sessionId) return null;
const session = sessions.get(sessionId);
if (!session) return null;
// Check expiry
if (Date.now() > session.expiresAt) {
sessions.delete(sessionId);
return null;
}
return session.data;
}
function destroySession(req, res) {
const cookies = parseCookies(req);
const sessionId = cookies[SESSION_COOKIE];
if (sessionId) sessions.delete(sessionId);
setCookie(res, SESSION_COOKIE, '', { maxAge: 0, path: '/' });
}
// Cleanup expired sessions periodically
setInterval(() => {
const now = Date.now();
for (const [id, session] of sessions) {
if (now > session.expiresAt) sessions.delete(id);
}
}, 60_000); // every minute
module.exports = { createSession, getSession, destroySession };
HttpOnly + SameSite:
HttpOnly prevents JavaScript from reading the cookie (mitigates XSS attacks). SameSite=Lax prevents the cookie from being sent in cross-site requests (mitigates CSRF attacks). Always set both for session cookies.
Login Handler
// handlers/auth.js
const { createSession, destroySession } = require('../session');
// Simple in-memory user store (use a real DB in production)
const users = new Map([
['admin', { password: 'secret', name: 'Admin User' }]
]);
async function handleLogin(req, res) {
const body = await readBody(req);
const params = new URLSearchParams(body);
const username = params.get('username');
const password = params.get('password');
const user = users.get(username);
if (!user || user.password !== password) {
res.writeHead(401, { 'Content-Type': 'text/html' });
res.end('<p>Invalid credentials</p>');
return;
}
createSession(res, { username, name: user.name });
res.writeHead(303, { Location: '/dashboard' });
res.end();
}
function handleLogout(req, res) {
destroySession(req, res);
res.writeHead(303, { Location: '/login' });
res.end();
}
module.exports = { handleLogin, handleLogout };
Protecting Routes
// middleware/auth.js
const { getSession } = require('../session');
function requireAuth(handler) {
return function(req, res, params) {
const session = getSession(req);
if (!session) {
res.writeHead(303, { Location: '/login' });
res.end();
return;
}
// Attach session to request for use in handler
req.session = session;
return handler(req, res, params);
};
}
// Usage in router
router.add('GET', '/dashboard', requireAuth(handleDashboard));
router.add('GET', '/products', requireAuth(handleProducts));
Production warning: This in-memory session store resets on server restart. For production, persist sessions in Redis or a database. Also use
bcrypt (or Node's built-in crypto.scrypt) for password hashing — never store plain text passwords.
Key takeaways:
- Parse cookies by splitting the
Cookieheader on;then= - Session IDs should be
crypto.randomBytes(32)— 256 bits of entropy - Always set
HttpOnlyandSameSite=Laxon session cookies Mapis a fast in-memory session store; use Redis in production- Higher-order functions make great auth middleware
What's Next
In Post #11, we add product images to our store — implementing multipart form uploads and image serving without any packages.