Cookies and Sessions

Intermediate
13 min

Cookies and Sessions

HTTP is stateless: the server forgets a client the moment a response is sent. Cookies let the browser carry a small piece of state back on every request, and sessions build on that to keep richer data on the server keyed by a cookie. After this lesson you will be able to set and read cookies, sign them against tampering, store server-side sessions in memory or a database, and pick the cookie flags that keep both safe.

Setting and clearing cookies

Express can set cookies without any package through res.cookie():

javascript
app.get("/theme/:name", (req, res) => { res.cookie("theme", req.params.name, { maxAge: 1000 * 60 * 60 * 24 * 30, // 30 days, in milliseconds httpOnly: false, // readable by page JavaScript sameSite: "lax", }); res.redirect("/"); }); app.get("/logout-theme", (req, res) => { res.clearCookie("theme"); res.sendStatus(204); });

The important options:

| Option | Meaning | | --- | --- | | maxAge / expires | Lifetime; omit both for a session cookie that dies with the browser | | httpOnly | Hidden from document.cookie, which blocks XSS from stealing it | | secure | Sent only over HTTPS | | sameSite | "lax" (default-safe), "strict", or "none" (requires secure) | | signed | Attach an HMAC so the value cannot be altered by the client | | domain / path | Limit where the browser sends the cookie |

In Express 5, res.clearCookie() ignores maxAge and expires, so pass the same path and domain you used when setting the cookie, nothing else.

Reading cookies with cookie-parser

Incoming cookies arrive as one Cookie header string. cookie-parser turns it into req.cookies and verifies signed cookies into req.signedCookies:

bash
npm install cookie-parser
javascript
import cookieParser from "cookie-parser"; app.use(cookieParser(process.env.COOKIE_SECRET)); app.get("/", (req, res) => { const theme = req.cookies.theme ?? "light"; const userId = req.signedCookies.uid; // undefined if tampered with res.send(`Theme: ${theme}, user: ${userId ?? "guest"}`); }); app.post("/remember", (req, res) => { res.cookie("uid", "42", { signed: true, httpOnly: true }); res.sendStatus(204); });

Signing does not encrypt: the value is still visible to the user, it just cannot be changed without invalidating the signature.

Server-side sessions

A cookie has a 4 KB limit and is visible to the client. Sessions keep the real data on the server and give the browser only an opaque session ID cookie (connect.sid by default).

bash
npm install express-session
javascript
import session from "express-session"; app.set("trust proxy", 1); // behind Nginx, Render, Railway, etc. app.use(session({ secret: process.env.SESSION_SECRET, resave: false, // do not rewrite unchanged sessions saveUninitialized: false, // no cookie until something is stored cookie: { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", maxAge: 1000 * 60 * 60 * 24, }, })); app.post("/cart", express.json(), (req, res) => { req.session.cart ??= []; req.session.cart.push(req.body.productId); res.json({ items: req.session.cart.length }); }); app.post("/logout", (req, res, next) => { req.session.destroy((err) => (err ? next(err) : res.sendStatus(204))); });

req.session is a plain object; assign to it and the middleware saves it at the end of the response. Call req.session.regenerate() right after a login to issue a fresh ID and prevent session fixation.

Persistent session stores

The default MemoryStore leaks memory and forgets everything on restart; express-session prints a warning if you use it in production. Plug in a store backed by your database:

javascript
import MongoStore from "connect-mongo"; app.use(session({ // ...same options as above store: MongoStore.create({ mongoUrl: process.env.MONGO_URI, ttl: 60 * 60 * 24 }), }));

connect-redis works the same way for Redis, and because every app instance reads the same store, sessions survive restarts and horizontal scaling.

Tips

  • Prefer httpOnly for anything security-related and reserve non-httpOnly cookies for UI preferences.
  • secure: true behind a reverse proxy only works after app.set("trust proxy", 1), otherwise Express thinks the request is plain HTTP and never sets the cookie.
  • Keep sessions small; store IDs and look up the rest per request.
Quick Quiz
Question 1 of 3

What does the `httpOnly` flag do?

Key Takeaways

  • res.cookie() and res.clearCookie() manage cookies; cookie-parser exposes them on req.cookies and req.signedCookies.
  • Use httpOnly, secure and sameSite on every sensitive cookie; signing prevents tampering but does not hide values.
  • Sessions store data server-side and hand the browser only an ID; configure resave: false and saveUninitialized: false.
  • Replace MemoryStore with connect-mongo or connect-redis before deploying.
  • Set trust proxy when running behind a reverse proxy so secure cookies work.

Next lesson: Connecting Express to MongoDB — connect your app to a real database with Mongoose and persist data across restarts.

Cookies and Sessions - Express.js | CodeYourCraft | CodeYourCraft