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:3000behind 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.