Working with Cookies
Cookies are small key-value strings the server asks the browser to store and send back on every subsequent request to the same site. They power sessions, authentication tokens, language preferences, and consent flags. Express ships with res.cookie() and res.clearCookie() for writing cookies, and the small cookie-parser middleware for reading them — including tamper-proof signed cookies. Setting the right options (httpOnly, secure, sameSite) is what separates a safe cookie from a security hole.
Setting a cookie with res.cookie
res.cookie(name, value, options) adds a Set-Cookie header to the response. The value can be a string or an object — objects are automatically JSON-serialized and prefixed with j: so they round-trip cleanly. The third argument is an options object that maps directly onto standard cookie attributes.
const express = require("express");
const app = express();
app.get("/login", (req, res) => {
res.cookie("sid", "abc123", {
httpOnly: true, // not readable from document.cookie
secure: true, // only sent over HTTPS
sameSite: "lax", // CSRF mitigation
maxAge: 1000 * 60 * 60 // 1 hour, in milliseconds
});
res.json({ loggedIn: true });
});
app.listen(3000);
Output (response header):
Set-Cookie: sid=abc123; Max-Age=3600; Path=/; Expires=...; HttpOnly; Secure; SameSite=Lax
Cookie options reference
| Option | Type | Effect |
|---|---|---|
httpOnly | boolean | Hides the cookie from client-side JavaScript (document.cookie). |
secure | boolean | Cookie is sent only over HTTPS connections. |
sameSite | "strict" | "lax" | "none" | Controls cross-site sending; "none" requires secure: true. |
maxAge | number | Lifetime in milliseconds (Express converts to seconds for the header). |
expires | Date | Absolute expiry; prefer maxAge, which is relative. |
path | string | URL path scope; defaults to /. |
domain | string | Domain scope; defaults to the host that set it. |
signed | boolean | Signs the cookie value (requires a secret — see below). |
Warning: Always set
httpOnly: truefor session and auth cookies. A cookie readable from JavaScript can be exfiltrated by any XSS payload on your page.
Reading cookies with cookie-parser
Express does not parse incoming cookies on its own. Install the cookie-parser middleware, which reads the request’s Cookie header and populates req.cookies.
npm install cookie-parser
Register it once with app.use, then every downstream handler can read req.cookies:
const cookieParser = require("cookie-parser");
app.use(cookieParser());
app.get("/dashboard", (req, res) => {
const sid = req.cookies.sid; // undefined if the cookie is absent
if (!sid) {
return res.status(401).json({ error: "Not authenticated" });
}
res.json({ session: sid });
});
Output (request carrying Cookie: sid=abc123):
{ "session": "abc123" }
Object-valued cookies set with res.cookie("prefs", { theme: "dark" }) are parsed back into real objects automatically, so req.cookies.prefs.theme returns "dark" without any manual JSON.parse.
Signed cookies
A plain cookie is fully editable by the client — nothing stops a user from changing sid=abc123 to someone else’s id. Signed cookies attach an HMAC signature so the server can detect tampering. Pass a secret to cookieParser(), then set the cookie with signed: true.
app.use(cookieParser("a-long-random-secret-string"));
app.get("/sign-in", (req, res) => {
res.cookie("uid", "42", { signed: true, httpOnly: true });
res.json({ ok: true });
});
app.get("/whoami", (req, res) => {
// Signed cookies live on req.signedCookies, NOT req.cookies
const uid = req.signedCookies.uid;
if (uid === undefined) {
return res.status(401).json({ error: "Invalid or tampered cookie" });
}
res.json({ uid });
});
If the value was altered in transit, req.signedCookies.uid is false (or absent), so a single equality check rejects tampered cookies. Note the strict separation: unsigned cookies appear on req.cookies, signed ones on req.signedCookies — never mix them up.
Tip: Signing proves a cookie was issued by your server; it does not encrypt the value. Treat the contents as readable by the client. For secrets, store an opaque session id and keep the real data server-side.
Clearing a cookie
To delete a cookie, call res.clearCookie(name, options). It emits a Set-Cookie header with an expiry in the past so the browser drops it. The browser only honors this if the path and domain match what was originally set, so pass the same options you used to create it.
app.post("/logout", (req, res) => {
res.clearCookie("sid", { httpOnly: true, sameSite: "lax", path: "/" });
res.json({ loggedOut: true });
});
Output (response header):
Set-Cookie: sid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Lax
Express 5 notes
Cookie handling is unchanged between Express 4 and 5: res.cookie() and res.clearCookie() keep identical signatures, and cookie-parser works the same in both. In Express 5, request properties are read-only getters, so continue treating req.cookies and req.signedCookies as values to read rather than mutate. One detail worth knowing across both versions: clearCookie ignores maxAge and expires in its options — it always sets an expired date — so you only need to match path, domain, and the security flags.
Best Practices
- Set
httpOnly: trueon every session and authentication cookie to block XSS theft. - Set
secure: truein production so cookies never travel over plain HTTP. - Default
sameSiteto"lax"; use"strict"for high-value actions and"none"(withsecure) only for genuine cross-site needs. - Sign any cookie whose value the client must not forge, and read it from
req.signedCookies. - Keep cookies small — store an opaque session id, not user data, and look the rest up server-side.
- Pass the same
path/domaintores.clearCookie()that you used inres.cookie(), or the deletion silently fails. - Never log raw
Cookieheaders; redact session values before they reach your logs.