This episode covers state management: the difference between stateless and stateful applications, server-side sessions with express-session, caching with Redis, and HTTP cache headers to speed up repeated responses.

An HTTP server should ideally be stateless — each request stands alone and doesn't depend on previous ones. In reality, applications need to remember things: shopping carts, login status, frequently requested data. This is where server-side state, sessions, and caching come in.
Episode 16 explains three layers of state: the difference between stateless and stateful applications, server-side sessions with express-session, caching with Redis for frequently accessed data, and HTTP cache headers so repeated responses don't need to be reprocessed.
A stateless application puts all state in the request itself — usually through a token like the JWT from episode 11. The advantage is huge: any server can serve any request, so horizontal scaling becomes simple and server restarts don't erase user data.
const payload = jwt.verify(token, process.env.JWT_SECRET);
const pengguna = await cariPengguna(payload.userId);jwt.verify(token, ...) returns an identity without needing to store a session on the server. The request carries everything itself. This is the favored pattern for modern APIs.
Some state is better kept on the server: login sessions that can be revoked at any time, per-user rate limiters, or work queues. When state is stored on the server, a request's identity is marked with a unique ID — usually via a cookie. This episode and episode 11 are two ends of a spectrum: JWT for stateless, sessions for stateful.
express-session stores the session on the server and sends a cookie containing the ID to the browser:
npm install express-sessionnpm install express-session adds the session middleware. In production, set up a Redis store so sessions can be shared across server instances.
import session from "express-session";
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: process.env.NODE_ENV === "production", httpOnly: true },
}),
);
app.post("/login", (req, res) => {
req.session.userId = 42;
res.json({ status: "success" });
});
app.get("/profil", (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: "Belum login" });
}
res.json({ userId: req.session.userId });
});req.session.userId = 42 stores session data, and req.session persists across requests through the cookie. The httpOnly: true attribute prevents browser JavaScript from reading the cookie, and secure: true forces HTTPS in production. The session can be deleted by the server at any time — an advantage over JWT, which can only expire over time.
Redis is an in-memory database often used as a cache. Data that's expensive to compute or fetch from the database is stored temporarily, so subsequent requests skip the processing:
npm install redisnpm install redis adds the official Node.js Redis client. Redis stores key-value pairs in memory, so reads and writes are much faster than database queries.
The most common pattern is cache-aside: check the cache first; if empty, fetch from the database and fill the cache:
import { createClient } from "redis";
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
async function ambilArtikel(id) {
const cache = await redis.get("artikel:" + id);
if (cache) return JSON.parse(cache);
const artikel = await cariArtikelDb(id);
await redis.set("artikel:" + id, JSON.stringify(artikel), { EX: 300 });
return artikel;
}redis.get("artikel:" + id) reads the cache; if present, it's returned directly. If not, the data is fetched from the database and then stored with EX: 300 (expires in 300 seconds). Notice the key with the artikel: prefix to avoid collisions between data types.
Besides caching in the application, HTTP itself has a caching mechanism through headers. Set the response with cache control so clients and proxies know which data can be stored:
app.get("/api/kota", (req, res) => {
res.setHeader("Cache-Control", "public, max-age=3600");
res.json(daftarKota);
});res.setHeader("Cache-Control", "public, max-age=3600") tells browsers and CDNs that the response is safe to copy for one hour. For private data, use private or no-store. Combining Redis cache-aside with HTTP headers can drastically reduce database load.
Start stateless, add sessions only if you really need them, and add caching after measuring that the database is the bottleneck. Premature optimization usually brings complexity without benefit.
Here's what to take away:
express-session stores sessions and sends an ID cookie.httpOnly and secure in production.Cache-Control header manages caching in browsers and CDNs.In the next episode, episode 17, we'll discuss file upload management and media streaming — processing multipart forms with multer, limiting file size and type, streaming files from disk, and supporting range requests for media. Your API will be ready to serve real files and media.