Series: Pure Node.js — Zero Dependencies

Post #10: Sessions & Cookies Without Packages

📅 March 2026 ⏱ 10 min read 🏷 Node.js, Sessions, Cookies, Auth

📚 Pure Node.js Series — Zero Dependencies

#1: Build an HTTP Server From Scratch #2: Manual Routing — No Express Needed #3: Serving Static Files (HTML, CSS, JS) #4: Handling Forms & POST Requests #5: JSON Files as a Database #6: HTML Templating with String Interpolation #7: Vanilla JS Charts & Dynamic UI #8: WebSockets From Scratch #9: Chat Rooms & Broadcast Messages → #10: Sessions & Cookies Without Packages (you are here) #11: File Uploads & Image Serving #12: Deploy Pure Node.js to a VPS

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 Cookie header on ; then =
  • Session IDs should be crypto.randomBytes(32) — 256 bits of entropy
  • Always set HttpOnly and SameSite=Lax on session cookies
  • Map is 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.

Building along? Share on X/Twitter or GitHub.