Skip to content
Express.js ex requests 4 min read

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.

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
OptionTypeEffect
httpOnlybooleanHides the cookie from client-side JavaScript (document.cookie).
securebooleanCookie is sent only over HTTPS connections.
sameSite"strict" | "lax" | "none"Controls cross-site sending; "none" requires secure: true.
maxAgenumberLifetime in milliseconds (Express converts to seconds for the header).
expiresDateAbsolute expiry; prefer maxAge, which is relative.
pathstringURL path scope; defaults to /.
domainstringDomain scope; defaults to the host that set it.
signedbooleanSigns the cookie value (requires a secret — see below).

Warning: Always set httpOnly: true for session and auth cookies. A cookie readable from JavaScript can be exfiltrated by any XSS payload on your page.

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.

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: true on every session and authentication cookie to block XSS theft.
  • Set secure: true in production so cookies never travel over plain HTTP.
  • Default sameSite to "lax"; use "strict" for high-value actions and "none" (with secure) 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/domain to res.clearCookie() that you used in res.cookie(), or the deletion silently fails.
  • Never log raw Cookie headers; redact session values before they reach your logs.
Last updated June 14, 2026
Was this helpful?