Express is minimal by design: it doesn’t add security features unless you ask. A production Express API needs a handful of middleware and settings to defend against common attacks, plus some operational hygiene. Here’s a practical checklist.

General API advice: API security best practices.

1. Security headers with helmet

import helmet from "helmet";
app.use(helmet());

Helmet sets sensible headers: Content-Security-Policy, X-Content-Type-Options, Strict-Transport-Security, Referrer-Policy, and more. Adjust the CSP for frontends that load external scripts. Also app.disable("x-powered-by") (helmet does this).

2. Behind Nginx: trust the proxy

app.set("trust proxy", 1);     // exactly one proxy in front (Nginx)

Without it, req.ip is always 127.0.0.1 (which breaks rate limiting and logs), and secure cookies don’t work over HTTPS terminated by Nginx. Don’t set trust proxy to true blindly: clients could spoof X-Forwarded-For. See Nginx reverse proxy.

3. Rate limiting

import rateLimit from "express-rate-limit";
app.use("/api/", rateLimit({ windowMs: 60_000, limit: 100, standardHeaders: "draft-7", legacyHeaders: false }));
app.use("/api/auth/login", rateLimit({ windowMs: 15 * 60_000, limit: 10 }));

With several instances (cluster mode), use a Redis store, so limits are shared. Add Nginx limit_req as a first line of defence. See Nginx rate limiting and API rate limiting.

4. Validate all input

Never trust req.body, req.query or req.params. Validate with a schema library:

import { z } from "zod";
const CreateUser = z.object({ email: z.string().email(), name: z.string().min(1).max(100) });

app.post("/api/users", express.json({ limit: "100kb" }), (req, res) => {
  const parsed = CreateUser.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: "invalid input" });
  // use parsed.data
});
  • Limit body sizes (express.json({ limit })).
  • Use parameterised queries or an ORM. Never concatenate SQL strings.
  • For MongoDB, reject objects in places where you expect strings (operator injection: {"$gt": ""}).

5. CORS: allow only your origins

import cors from "cors";
app.use(cors({ origin: ["https://app.example.com"], credentials: true }));

origin: "*" with credentials is a common, dangerous misconfiguration.

6. Authentication, sessions and cookies

app.use(session({
  store: new RedisStore({ client: redis }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { secure: true, httpOnly: true, sameSite: "lax", maxAge: 86400000 },
}));
  • Hash passwords with bcrypt or argon2.
  • For JWTs: short expiry, a strong secret or asymmetric keys, and verify the algorithm.
  • Add CSRF protection for cookie-based auth on state-changing routes.

See Node.js environment variables for keeping secrets out of code.

7. Error handling without leaks

app.use((err, req, res, next) => {
  req.log?.error(err);                               // log details server-side
  res.status(err.status || 500).json({ error: "internal error" });   // no stack traces to clients
});

Run with NODE_ENV=production, so Express doesn’t expose stack traces. Track errors with an error tracker. See error tracking with Sentry.

8. Dependencies

npm audit --omit=dev
npm outdated

Enable Dependabot or Renovate, pin versions with a lockfile, and remove unused packages. Most Node.js vulnerabilities come from dependencies.

9. Run safely on the server

  • Run as an unprivileged user, never root. Bind to 127.0.0.1:3000 behind Nginx, which handles TLS on 443. See Linux capabilities.
  • Use PM2 or systemd for restarts, and limit memory. See PM2.
  • Firewall everything except 22, 80 and 443. See UFW.

10. Logging and monitoring

Log requests (method, path, status, duration, IP), but not passwords, tokens or full request bodies. Alert on spikes in 4xx/5xx responses and auth failures. See log management.

Quick reference

Area Tool / setting
Headers helmet()
Proxy app.set("trust proxy", 1)
Rate limits express-rate-limit (+ Redis store)
Validation zod / joi, body size limits
CORS Explicit origin list
Cookies secure, httpOnly, sameSite
Dependencies npm audit, Dependabot

Related: Node.js cluster mode and deploy a MERN app.

  • Node.js
  • Security
  • API