HTTP

Guide

HTTP Sessions and State Management Explained

Learn how to manage user state and sessions in stateless HTTP applications using cookies, tokens, and server-side storage.

7 min read intermediate Try in Playground

TL;DR: HTTP is stateless, so sessions use cookies or tokens to maintain user state across requests. Server-side sessions store data securely, while JWT tokens are stateless and self-contained.

HTTP is a stateless protocol, meaning each request is independent and the server doesn’t remember previous interactions. Sessions and state management solve this problem by maintaining context across multiple requests, enabling features like user authentication, shopping carts, and personalized experiences.

Introduction

Imagine walking into a store where the staff forgets you every time you blink. That’s HTTP without state management. Sessions provide memory, allowing web applications to remember who you are and what you’re doing.

Why state management matters:

  • Authentication: Remember logged-in users
  • Shopping carts: Track items across pages
  • Multi-step forms: Preserve progress through wizards
  • User preferences: Remember settings and customizations
  • Analytics: Track user journeys and behavior

The State Management Problem

HTTP is stateless:

Request 1: User logs in    → Server: "Welcome!"
Request 2: View profile    → Server: "Who are you?"
Request 3: Update settings → Server: "I don't know you"

With state management:

Request 1: User logs in    → Server: "Welcome! Here's your session ID"
Request 2: View profile    → Server: "You're John, here's your profile"
Request 3: Update settings → Server: "Settings updated for John"

State Management Approaches

1. Server-Side Sessions

The server stores session data and sends a session ID to the client.

How it works:

1. User logs in
2. Server creates session with unique ID
3. Server stores session data (in-memory, Redis, database)
4. Server sends session ID to client (usually as cookie)
5. Client includes session ID in subsequent requests
6. Server retrieves session data using the ID

Implementation (Node.js/Express):

const session = require('express-session')
const RedisStore = require('connect-redis')(session)
const redis = require('redis')

const redisClient = redis.createClient()

app.use(
  session({
    store: new RedisStore({ client: redisClient }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      secure: true, // HTTPS only
      httpOnly: true, // No JavaScript access
      maxAge: 86400000, // 24 hours
      sameSite: 'strict' // CSRF protection
    }
  })
)

// Store data in session
app.post('/login', (req, res) => {
  const { username, password } = req.body

  if (authenticate(username, password)) {
    req.session.userId = getUserId(username)
    req.session.username = username
    req.session.loginTime = Date.now()

    res.json({ success: true })
  } else {
    res.status(401).json({ error: 'Invalid credentials' })
  }
})

// Access session data
app.get('/profile', (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Not logged in' })
  }

  res.json({
    username: req.session.username,
    userId: req.session.userId
  })
})

// Update session data
app.post('/preferences', (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Not logged in' })
  }

  req.session.preferences = req.body
  res.json({ success: true })
})

// Destroy session
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({ success: true })
  })
})
```text

**Pros:**

- Secure (data stored server-side)
- Can store unlimited data
- Easy to invalidate
- Works automatically with cookies

**Cons:**

- Requires server-side storage
- Scaling challenges (need shared session store)
- Memory consumption

### 2. Token-Based State (JWT)

The server creates a signed token containing user data, which the client stores and sends with each request.

**How it works:**

```text
1. User logs in
2. Server creates JWT with user data
3. Server signs JWT with secret key
4. Client stores JWT (localStorage, cookie)
5. Client sends JWT in Authorization header
6. Server verifies signature and extracts data
```javascript

**Implementation:**

```javascript
const jwt = require('jsonwebtoken')

// Create token on login
app.post('/login', (req, res) => {
  const { username, password } = req.body

  if (authenticate(username, password)) {
    const user = getUserByUsername(username)

    const token = jwt.sign(
      {
        userId: user.id,
        username: user.username,
        role: user.role
      },
      process.env.JWT_SECRET,
      { expiresIn: '24h' }
    )

    res.json({ token })
  } else {
    res.status(401).json({ error: 'Invalid credentials' })
  }
})

// Verify token middleware
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers.authorization
  const token = authHeader && authHeader.split(' ')[1]

  if (!token) {
    return res.status(401).json({ error: 'No token provided' })
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'Invalid token' })
    }

    req.user = user
    next()
  })
}

// Use middleware
app.get('/profile', authenticateToken, (req, res) => {
  res.json({
    userId: req.user.userId,
    username: req.user.username,
    role: req.user.role
  })
})

// Refresh token
app.post('/refresh', authenticateToken, (req, res) => {
  const newToken = jwt.sign(
    { userId: req.user.userId, username: req.user.username, role: req.user.role },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  )

  res.json({ token: newToken })
})

Client usage:

// Store token
localStorage.setItem('token', response.token)

// Send token with requests
fetch('/api/profile', {
  headers: {
    Authorization: `Bearer ${localStorage.getItem('token')}`
  }
})
```javascript

**Pros:**

- Stateless (no server storage needed)
- Scales easily
- Works across domains
- Self-contained

**Cons:**

- Cannot invalidate before expiration
- Token size (sent with every request)
- Vulnerable to XSS if stored in localStorage

### 3. Client-Side Storage

Store state in the browser (cookies, localStorage, sessionStorage).

**Cookies:**

```javascript
// Server sets cookie
res.cookie('preferences', JSON.stringify(userPrefs), {
  maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
  httpOnly: false, // Allow JavaScript access
  secure: true
})

// Client reads cookie
const preferences = JSON.parse(getCookie('preferences'))

function getCookie(name) {
  const value = `; ${document.cookie}`
  const parts = value.split(`; ${name}=`)
  if (parts.length === 2) return parts.pop().split(';').shift()
}

localStorage (persists after browser close):

// Store data
localStorage.setItem('theme', 'dark')
localStorage.setItem('cart', JSON.stringify(cartItems))

// Retrieve data
const theme = localStorage.getItem('theme')
const cart = JSON.parse(localStorage.getItem('cart'))

// Remove data
localStorage.removeItem('theme')

// Clear all
localStorage.clear()
```javascript

**sessionStorage (cleared when tab closes):**

```javascript
// Temporary form data
sessionStorage.setItem('formData', JSON.stringify(formValues))

// Retrieve on page reload
const saved = JSON.parse(sessionStorage.getItem('formData'))

Pros:

  • No server requests needed
  • Fast access
  • Simple to implement

Cons:

  • Limited to ~5-10MB
  • Visible to user
  • Vulnerable to XSS
  • Only strings (need JSON.stringify/parse)

4. Hybrid Approach

Combine server sessions with client storage for optimal UX.

Example:

// Server-side session for authentication
req.session.userId = user.id

// Client-side storage for UI preferences
localStorage.setItem('theme', 'dark')
localStorage.setItem('language', 'en')

// Use both
app.get('/dashboard', (req, res) => {
  if (!req.session.userId) {
    return res.redirect('/login')
  }

  const user = getUserById(req.session.userId)
  const theme = req.cookies.theme || 'light'

  res.render('dashboard', { user, theme })
})
```text

## Session Storage Options

### 1. In-Memory (Development Only)

```javascript
app.use(
  session({
    secret: 'dev-secret',
    resave: false,
    saveUninitialized: false
  })
)

Pros: Fast, simple Cons: Lost on restart, doesn’t scale

const redis = require('redis')
const RedisStore = require('connect-redis')(session)

const redisClient = redis.createClient({
  host: 'localhost',
  port: 6379
})

app.use(
  session({
    store: new RedisStore({ client: redisClient }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: { maxAge: 86400000 }
  })
)
```javascript

**Pros**: Fast, persistent, scalable
**Cons**: Extra infrastructure

### 3. Database (PostgreSQL, MongoDB)

```javascript
const MongoStore = require('connect-mongo')

app.use(
  session({
    store: MongoStore.create({
      mongoUrl: process.env.MONGODB_URI,
      ttl: 24 * 60 * 60 // 24 hours
    }),
    secret: process.env.SESSION_SECRET
  })
)

Pros: Persistent, can query sessions Cons: Slower than Redis

Security Best Practices

1. Secure Session IDs

// Use cryptographically strong session IDs
const crypto = require('crypto')
const sessionId = crypto.randomBytes(32).toString('hex')

// Regenerate session ID on login
app.post('/login', (req, res) => {
  req.session.regenerate((err) => {
    if (err) return next(err)
    req.session.userId = user.id
    res.json({ success: true })
  })
})
```http

### 2. HttpOnly Cookies

```javascript
cookie: {
  httpOnly: true,  // Prevent JavaScript access
  secure: true,    // HTTPS only
  sameSite: 'strict'  // CSRF protection
}

3. Session Expiration

// Absolute timeout (24 hours)
cookie: {
  maxAge: 24 * 60 * 60 * 1000
}

// Sliding timeout (extends on activity)
app.use((req, res, next) => {
  if (req.session) {
    req.session.touch()
  }
  next()
})

// Check session age
app.use((req, res, next) => {
  if (req.session && req.session.loginTime) {
    const age = Date.now() - req.session.loginTime
    const maxAge = 24 * 60 * 60 * 1000

    if (age > maxAge) {
      req.session.destroy()
      return res.status(401).json({ error: 'Session expired' })
    }
  }
  next()
})
```javascript

### 4. CSRF Protection

```javascript
const csrf = require('csurf')

app.use(csrf({ cookie: true }))

// Send CSRF token to client
app.get('/form', (req, res) => {
  res.render('form', { csrfToken: req.csrfToken() })
})

// Include in forms
<form method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}">
  <!-- form fields -->
</form>

Shopping Cart Example

Complete shopping cart implementation using sessions:

// Add to cart
app.post('/cart/add', (req, res) => {
  const { productId, quantity } = req.body

  if (!req.session.cart) {
    req.session.cart = []
  }

  const existing = req.session.cart.find((item) => item.productId === productId)

  if (existing) {
    existing.quantity += quantity
  } else {
    req.session.cart.push({ productId, quantity })
  }

  res.json({ cart: req.session.cart })
})

// View cart
app.get('/cart', (req, res) => {
  const cart = req.session.cart || []
  const items = cart.map((item) => ({
    ...getProduct(item.productId),
    quantity: item.quantity
  }))

  res.json({ items })
})

// Remove from cart
app.delete('/cart/:productId', (req, res) => {
  const { productId } = req.params

  if (req.session.cart) {
    req.session.cart = req.session.cart.filter((item) => item.productId !== productId)
  }

  res.json({ cart: req.session.cart })
})

// Checkout
app.post('/checkout', (req, res) => {
  if (!req.session.cart || req.session.cart.length === 0) {
    return res.status(400).json({ error: 'Cart is empty' })
  }

  const order = createOrder(req.session.userId, req.session.cart)
  req.session.cart = [] // Clear cart

  res.json({ order })
})

Frequently Asked Questions

Why is HTTP stateless?

HTTP treats each request independently with no memory of previous requests. This simplifies servers but requires mechanisms like cookies to maintain user sessions.

How do sessions work in HTTP?

Server creates a session ID, sends it via Set-Cookie, client sends it back with each request. Server uses the ID to retrieve stored session data.

What is the difference between cookies and sessions?

Cookies store data client-side. Sessions store data server-side with only an ID in the cookie. Sessions are more secure for sensitive data.

What are alternatives to cookie sessions?

JWT tokens (stateless, self-contained), URL parameters (not recommended), localStorage with Authorization header. Each has different security tradeoffs.

Keep Learning