Demystifying Cookies and Sessions in Node.js: A Journey from Raw HTTP to Express
Demystifying Cookies and Sessions in Node.js As web developers, we often encounter cookies and sessions neatly packaged in libraries like express-session or...

As web developers, we often encounter cookies and sessions neatly packaged in libraries like express-session or cookie-parser. I've frequently found myself explaining these concepts to colleagues, especially junior devs, in the organizations I've worked for. Truth be told, these ideas were a bit murky for me early on, too. However, grasping their fundamentals has not only helped me teach them but also allowed me to explain them clearly to anyone needing assistance.
Many developers view sessions and cookies as a single, tangled concept that's hard to untangle. Understandably so—they're closely related. Yet, understanding each separately is crucial for mastering authentication and state management in web applications.
In this post, we’ll strip away the convenience of packages and build our understanding from the ground up using Node.js’s native http module. By the end, you'll clearly understand:
- What cookies are and how they function at the HTTP level.
- How sessions leverage cookies for secure state management.
- How to implement both without external packages (not to reinvent the wheel, but to deepen our understanding).
The Cookie Conundrum
At its core, a cookie is a small piece of data a server asks a browser to store and return with future requests. That’s it! The concept is elegantly simple, but the details can get tricky.
The server sets a Set-Cookie header in its response, and the client sends this data back in subsequent requests to the same server.
Set-Cookie: mycookie=value; Path=/; HttpOnlyLet’s create a basic HTTP server to demonstrate cookies using raw Node.js:
const http = require("http");
const url = require("url");
// Create a simple HTTP server
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const path = parsedUrl.pathname;
// Parse cookies from the request headers
const cookies = CookieManager.parse(req);
// Home page
if (path === "/") {
// Render home page and show all cookies (if any)
ServerService.handleServeRoot(res, cookies);
} else if (path === "/dashboard") {
// Dashboard - set cookies from query parameters
const query = parsedUrl.query;
// Set cookies based on query parameters
CookieManager.setFromQuery(res, query);
// Render dashboard and show all cookies
ServerService.handleServeDashboard(res);
} else if (path === "/dashboard-session") {
// Handle session creation and management
SessionManager.handleSession(req, res, parsedUrl);
// Render session dashboard
ServerService.handleServeDashboardSession(res, cookies.sessionId);
} else if (path === "/dashboard-secure") {
// Render secure dashboard
ServerService.handleServeDashboardSecure(res, cookies.sessionId);
} else {
// Handle 404 Not Found
ServerService.handleServeNotFound(res);
}
});
server.listen(3006, () => console.log("Server running at http://localhost:3006/"));What Does This Server Do?
In simple terms, our server:
- Renders HTML at /, displaying any cookies sent by the client.
- Sets cookies in HTTP response headers based on query parameters sent to /dashboard (A simplified example).
- Creates a session ID and stores session data from query parameters at /dashboard-session, then uses this data at /dashboard-secure to simulate an authenticated endpoint. (Note: We'd never send session data to the client in a real application.)
Breaking Down the Server
The server uses object composition patterns, offering several benefits:
- Encapsulation: Related methods are grouped together.
- Reusability: Components like CookieManager can be reused across the app.
- Extensibility: New features can be added without altering existing code.
- Testability: Each method can be tested independently.
“Object composition focuses on assembling objects with specific behaviors rather than inheritance hierarchies. This ‘has-a’ relationship, rather than ‘is-a,’ often leads to more flexible code.”
For more on object composition:
- JavaScript Composition vs Inheritance
- The Composition Over Inheritance Principle
- Eric Elliott’s “Composing Software” series
In the ServerService:
const ServerService = {
handleServeRoot(res, cookies) {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(`
Cookie Demo
${
Object.keys(cookies).length === 0
? "No cookies set"
: `
Current cookies on this Domain:
${JSON.stringify(cookies, null, 2)}
`
}
Set a cookie
`);
},
handleServeDashboard(res) {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(`
Dashboard
Cookies set! Your browser now has cookies:
Go back home
Create a session
`);
},
handleServeDashboardSession(res, sessionId) {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(`
Session Dashboard
Your browser cookie contains sessionId: ${sessionId}
But your sensitive data is stored on the server:
Go back home
View secure dashboard
`);
},
handleServeDashboardSecure(res, sessionId) {
if (sessionId && SessionManager.sessions[sessionId]) {
const sessionData = SessionManager.sessions[sessionId];
res.writeHead(200, { "Content-Type": "text/html" });
res.end(`
Secure Dashboard
Your session ID: ${sessionId}
Your session data:
${JSON.stringify(sessionData, null, 2)}
Go back home
`);
} else {
res.writeHead(403, { "Content-Type": "text/html" });
res.end(`
Access Denied
You need to create a session first.
Go back home
`);
}
},
handleServeNotFound(res) {
res.writeHead(404);
res.end("Not found");
}
};- handleServeRoot: Renders the home page, showing cookies if present, with a link to /dashboard.
- handleServeDashboard: Renders the dashboard, confirming cookies are set, with links to home or session creation.
- handleServeDashboardSession: Displays the session ID and notes that sensitive data is server-side.
- handleServeDashboardSecure: Simulates a logged-in user, verifying the session ID and showing data or denying access.
- handleServeNotFound: Handles undefined routes with a 404 response.
- The CookieManager handles parsing cookies, both from query params and from the request headers and also setting cookies in the request headers
const CookieManager = {
// Parse cookies from request
parse(req) {
const cookieHeader = req.headers.cookie;
const cookies = {};
if (cookieHeader) {
cookieHeader.split(";").forEach((cookie) => {
const [key, value] = cookie.trim().split("=");
cookies[key] = value;
});
}
return cookies;
},
// Create a cookie string with options
create(name, value, options = {}) {
let cookie = `${name}=${value}`;
if (options.httpOnly) cookie += "; HttpOnly";
if (options.secure) cookie += "; Secure";
if (options.path) cookie += `; Path=${options.path}`;
// Use Max-Age instead of Expires as it less error-prone, Max-Age takes precedence when both are set.
if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
if (options.maxAge) cookie += `; Max-Age=${options.maxAge}`;
if (options.domain) cookie += `; Domain=${options.domain}`;
if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
return cookie;
},
// Set a cookie in the response
set(res, name, value, options = {}) {
const cookie = this.create(name, value, options);
// Handle multiple Set-Cookie headers
let cookies = res.getHeader("Set-Cookie") || [];
if (!Array.isArray(cookies)) {
cookies = [cookies];
}
cookies.push(cookie);
res.setHeader("Set-Cookie", cookies);
return this;
},
// Set cookies from query parameters
setFromQuery(res, query) {
Object.entries(query).forEach(([key, value]) => {
this.set(res, key, value, {
httpOnly: true,
path: "/",
maxAge: 86400, // 1 day in seconds
});
});
}
};The SessionManager handles creating of session ID, storing and retrieving of session data.
const crypto = require("crypto");
const SessionManager = {
// In-memory storage for sessions
sessions: {},
// Function to handle session creation and management
handleSession(req, res, parsedUrl) {
const cookies = CookieManager.parse(req);
const query = parsedUrl.query;
// Check if the user already has a session ID cookie
let sessionId = cookies.sessionId;
if (!sessionId) {
// If no session exists, create a new one
sessionId = crypto.randomBytes(16).toString("hex");
CookieManager.set(res, "sessionId", sessionId, {
httpOnly: true,
path: "/",
});
this.sessions[sessionId] = {};
}
// Get or initialize the session data
const sessionData = this.sessions[sessionId] || {};
// Add any query parameters to the session data
if (Object.keys(query).length > 0) {
Object.entries(query).forEach(([key, value]) => {
sessionData[key] = value;
});
this.sessions[sessionId] = sessionData;
}
return this.sessions[sessionId] ?? null;
}
};How it all works together
When you first visit http://localhost:3006/, no cookies are present:

Clicking “Set a cookie” navigates to http://localhost:3006/dashboard?flavor=chocolate&type=chip. The server:
- Reads query parameters and sets cookies.
- The browser stores these cookies.
- Future requests include these cookies automatically.
Returning to / displays the cookies:

HTTP Breakdown
- Request: Browser requests /dashboard?flavor=chocolate&type=chip.
- Response: Server sets headers:
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: flavor=chocolate; HttpOnly; Path=/
Set-Cookie: type=chip; HttpOnly; Path=/- Subsequent Requests: Browser includes:
Cookie: flavor=chocolate; type=chipThat’s the core mechanism! Everything else is an abstraction.
Note: Cookies are visible in the HTML because the server renders them. With HttpOnly, they're inaccessible to client-side JavaScript but visible in the browser's dev tools.
Cookies vs. Sessions: The Crucial Difference
Cookies store data in the browser, sent with every request. Sessions store data on the server, with only a reference ID in the cookie.
Cookies:
- Accessible via JavaScript (unless HttpOnly).
- Viewable by users or anyone with access to the client.
- Can be tampered with (unless signed).
- Limited to ~4KB per domain.
Sessions:
- Store only an ID in the cookie.
- Keep sensitive data server-side, using the ID as a key for server memory or database lookup.
In our example, visiting /dashboard-session?credit_card=1234-5678-9012-3456 stores or with other params, e.g /dashboard-session?credit_card=1234-5678-9012-3456&pin=1234&security_number=2234, stores these data server-side, not in the browser. The cookie holds only a random sessionId. At /dashboard-secure, the server uses this ID to retrieve the data.


Caution: Never send sensitive data like credit card details in query parameters in production.
This approach enables features like shopping carts, user preferences, and authentication without storing sensitive data client-side.
Advancing to Express
Now that we understand the raw mechanics, let’s see how Express simplifies things:
const express = require('express');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const crypto = require('crypto');
const app = express();
// Middleware to parse cookies
app.use(cookieParser());
// Middleware to handle sessions
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: { secure: false } // Set to true if using HTTPS
}));
// Home route
app.get('/', (req, res) => {
// Render home page and show all cookies (if any)
ServerService.handleServeRoot(res, req.cookies);
});
// Dashboard route - demonstrates cookies
app.get('/dashboard', (req, res) => {
// Set cookies based on query parameters
Object.entries(req.query).forEach(([key, value]) => {
res.cookie(key, value);
});
// Set cookies based on query parameters
CookieManager.setFromQuery(res, req.query, true);
// Render dashboard and show all cookies
ServerService.handleServeDashboard(res);
});
// Session route - demonstrates sessions
app.get('/dashboard-session', (req, res) => {
// Handle session creation and management
SessionManager.handleSession(req, res, null, true);
// Render session dashboard
ServerService.handleServeDashboardSession(res, req.session.id);
});
// Secure dashboard - shows how to use session data
app.get('/dashboard-secure', (req, res) => {
if (!req.session.credit_card) {
return res.redirect('/dashboard-session?message=Please+set+credit+card+first');
}
// Render secure dashboard
ServerService.handleServeDashboardSecure(res, req.session.id, req);
});
// Start the server
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Express server running at http://localhost:${PORT}/`);
});A few modifications to our earlier Object compositions:
const SessionManager = {
// ... other methods, objects unchanged ...
handleSession(req, res, parsedUrl, isExpress) {
if (isExpress) {
// Add query parameters to the session
Object.entries(req.query).forEach(([key, value]) => {
req.session[key] = value;
});
return req.session;
}
const cookies = CookieManager.parse(req);
const query = parsedUrl.query;
// Check if the user already has a session ID cookie
let sessionId = cookies.sessionId;
if (!sessionId) {
// If no session exists, create a new one
sessionId = crypto.randomBytes(16).toString("hex");
CookieManager.set(res, "sessionId", sessionId, {
httpOnly: true,
path: "/",
});
this.sessions[sessionId] = {};
}
// Get or initialize the session data
const sessionData = this.sessions[sessionId] || {};
// Add any query parameters to the session data
if (Object.keys(query).length > 0) {
Object.entries(query).forEach(([key, value]) => {
sessionData[key] = value;
});
this.sessions[sessionId] = sessionData;
}
return this.sessions[sessionId] ?? null;
}
};
const ServerService = {
// ... other methods unchanged ...
handleServeDashboardSecure(res, sessionId, req) {
if (!req) {
if (sessionId && SessionManager.sessions[sessionId]) {
const sessionData = SessionManager.sessions[sessionId];
res.writeHead(200, { "Content-Type": "text/html" });
res.end(`
Secure Dashboard
Your session ID: ${sessionId}
Your session data:
${JSON.stringify(sessionData, null, 2)}
Go back home
`);
} else {
res.writeHead(403, { "Content-Type": "text/html" });
res.end(`
Access Denied
You need to create a session first.
Go back home
`);
}
} else {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(`
Secure Dashboard
Your session ID: ${sessionId}
Your session data:
${JSON.stringify(req.session, null, 2)}
Go back home
`);
}
}
};If we so desired, we could turn the CookieManager.parse into an express middleware, and it would serve the same purpose as app.use(cookieParser()) via app.use(CookieManager.parse).
But this tutorial is already long enough, and let’s stick to what you’d normally see in an Express/Node server.
The Big Picture
- Cookies: HTTP headers where the server says, “Store this,” and the browser complies.
- Sessions: Use cookies to store an ID, keeping sensitive data server-side.
- Raw HTTP: Verbose, but reveals the simplicity of cookies.
- Express: Middleware like cookie-parser and express-session handles the heavy lifting.
When to Use What
- Cookies: Ideal for non-sensitive data like preferences, language settings, or themes.
- Sessions: Essential for authentication, shopping carts, or sensitive data.
Security Considerations
- Use HttpOnly for session ID cookies to prevent JavaScript access.
- Enable Secure to ensure cookies are sent only over HTTPS.
- Implement CSRF protection, as cookies are sent automatically.
- Set appropriate expiration times based on data sensitivity.
- Prefer Max-Age over Expires for reliability.
- Use signed cookies to prevent tampering.
Conclusion
Understanding cookies and sessions at their core equips us to build secure, efficient state management. While frameworks abstract complexity, knowing the underlying mechanics helps us make informed decisions and debug effectively. Next time you npm install express-session or cookie-parser, you'll appreciate the magic it performs—and maybe smile at how it once seemed daunting.
Got thoughts or questions? Leave a comment and share your insights!