Skip to content
Express.js ex libraries 4 min read

express-session

HTTP is stateless, so without help the server forgets who a user is between requests. Sessions solve this by storing state on the server and handing the browser a small, signed cookie that points at it. The express-session middleware wires this up for you: it parses the session cookie, loads the matching record from a store, and exposes it as req.session so your handlers can read and mutate per-user data — a logged-in user id, a shopping cart, a CSRF token — without putting any of it in the cookie itself.

Installing and configuring

Install the package and mount it as middleware. The only required option is secret, which signs the session id cookie so clients cannot forge or tamper with it. The two boolean flags below — resave and saveUninitialized — should almost always be set explicitly, because their defaults are deprecated and noisy.

npm install express-session
const express = require("express");
const session = require("express-session");

const app = express();

app.use(
  session({
    secret: process.env.SESSION_SECRET, // sign the session id cookie
    resave: false, // don't re-save unchanged sessions
    saveUninitialized: false, // don't store empty sessions
    cookie: {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "lax",
      maxAge: 1000 * 60 * 60 * 24, // 1 day
    },
  })
);

app.listen(3000, () => console.log("Listening on http://localhost:3000"));

With saveUninitialized: false, a session record is only persisted once you actually write something to req.session, which keeps your store free of empty rows for bots and anonymous visitors.

Never hard-code the secret. Read it from an environment variable, and rotate it by passing an array (secret: [newSecret, oldSecret]) so existing cookies signed with the old value still verify while new ones use the first entry.

Reading and mutating req.session

Inside any handler, req.session is a plain object you can read from and assign to. Anything you set is automatically saved back to the store at the end of the request. Two properties are reserved: req.session.id (the session id) and req.session.cookie (the per-session cookie settings).

app.use(express.json());

// Log in: write data onto the session
app.post("/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  if (!user) return res.status(401).json({ error: "Invalid credentials" });

  req.session.userId = user.id;
  req.session.role = user.role;
  res.json({ ok: true });
});

// Read it back on a protected route
app.get("/me", (req, res) => {
  if (!req.session.userId) return res.status(401).json({ error: "Not logged in" });
  res.json({ userId: req.session.userId, role: req.session.role });
});

To end a session, call req.session.destroy, which removes it from the store and clears the cookie.

app.post("/logout", (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: "Could not log out" });
    res.clearCookie("connect.sid");
    res.json({ ok: true });
  });
});

A successful login response sets the signed cookie:

Output:

< HTTP/1.1 200 OK
< Set-Cookie: connect.sid=s%3AaT9k...; Path=/; HttpOnly; SameSite=Lax
< Content-Type: application/json
{"ok":true}

The cookie object controls how the session id is stored in the browser. Getting these right is what separates a secure session from a hijackable one.

OptionTypePurpose
httpOnlybooleanHide the cookie from client JavaScript (default true)
securebooleanOnly send over HTTPS — required in production
sameSite"lax" / "strict" / "none"CSRF protection; "none" needs secure: true
maxAgenumberLifetime in milliseconds before the cookie expires
domainstringScope the cookie to a domain and its subdomains
pathstringURL path the cookie applies to (default /)

When you run behind a reverse proxy or load balancer that terminates TLS (Nginx, Heroku, AWS ALB), set app.set("trust proxy", 1) so Express recognizes the original https scheme and the secure cookie is actually sent.

Session stores

The default store is MemoryStore, which keeps sessions in process memory. It is fine for local development but leaks memory, does not survive restarts, and cannot be shared across multiple instances — express-session will warn you about this on boot. For production, use a shared external store such as Redis or MongoDB.

npm install connect-redis redis
const session = require("express-session");
const { RedisStore } = require("connect-redis");
const { createClient } = require("redis");

const redisClient = createClient({ url: process.env.REDIS_URL });
redisClient.connect().catch(console.error);

app.use(
  session({
    store: new RedisStore({ client: redisClient, prefix: "sess:" }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: { httpOnly: true, secure: true, maxAge: 1000 * 60 * 60 },
  })
);

For MongoDB, connect-mongo plugs in the same way and reuses an existing connection string:

const MongoStore = require("connect-mongo");

app.use(
  session({
    store: MongoStore.create({
      mongoUrl: process.env.MONGO_URL,
      ttl: 60 * 60 * 24, // expire after 1 day (seconds)
    }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
  })
);

A shared store means any instance behind your load balancer can serve any user’s request, and sessions persist across deploys and restarts.

Best Practices

  • Always set httpOnly: true, secure: true (in production), and sameSite to defend against XSS theft and CSRF.
  • Use a persistent store (Redis or Mongo) in production — never ship the default MemoryStore.
  • Keep resave: false and saveUninitialized: false to avoid race conditions and useless writes.
  • Load secret from the environment and support rotation by passing an array of secrets.
  • Call req.session.regenerate right after login to issue a fresh session id and prevent session fixation.
  • Set app.set("trust proxy", 1) when terminating TLS at a proxy so secure cookies are sent correctly.
  • Pick a maxAge (and store ttl) that balances convenience against exposure, and call req.session.destroy on logout.
Last updated June 14, 2026
Was this helpful?