Securing a Node.js app in production is mostly about getting a dozen unglamorous basics right: a supported Node version, patched dependencies, secrets out of Git, security headers, rate limits, input validation, proper password hashing, safe error handling and careful logging. This checklist walks through each item with working Express code for Node.js 22 or 24, so you can go through your app line by line before go-live.
1. Run a supported Node.js version
Only use an Active LTS or Maintenance LTS release of Node.js - currently the 22 and 24 lines. Odd-numbered releases are short-lived and end-of-life versions stop receiving security fixes. Check the release schedule on nodejs.org and pin the version in package.json:
{
"engines": { "node": ">=22" }
}
Set the same version on your host, and plan an upgrade before your version's end-of-life date.
2. Keep dependencies patched
Most Node.js vulnerabilities arrive through dependencies, not your own code.
- Commit
package-lock.jsonand install withnpm ciin production builds, so what you tested is what you run. - Run
npm audit --omit=devin CI and fix high and critical findings. - Turn on automated update pull requests (Dependabot, Renovate or similar).
- Remove packages you no longer use - each one is attack surface.
- Be wary of typo-squatted package names when adding new dependencies.
3. Keep secrets out of code
Database passwords, API keys and session secrets belong in environment variables, never in the repository. Node.js 22 and later can load a local .env file without extra packages:
node --env-file=.env server.js
Add .env to .gitignore, and set production values in your host's environment settings. On NewHost, environment variables are stored encrypted and injected at build and run time. If a secret ever lands in Git history, rotate it - deleting the commit is not enough.
4. Set security headers and hide the framework
The helmet package sets sensible defaults for headers such as Content-Security-Policy, Strict-Transport-Security and X-Content-Type-Options:
import express from 'express';
import helmet from 'helmet';
const app = express();
app.disable('x-powered-by');
app.use(helmet());
Tune the Content-Security-Policy for your front end; the default is strict and may block inline scripts you rely on. The MDN HTTP headers reference explains each header.
5. Force HTTPS and trust the proxy correctly
In production your app almost always sits behind a proxy or load balancer that terminates TLS. Tell Express so that req.ip and req.secure are correct - otherwise rate limiting sees every request coming from the proxy:
app.set('trust proxy', 1); // trust the first proxy hop only
Use HTTPS for every page (free Let's Encrypt certificates make this painless) and set cookies with secure, httpOnly and sameSite.
6. Limit request size and rate
import { rateLimit } from 'express-rate-limit';
app.use(express.json({ limit: '100kb' }));
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 10,
standardHeaders: 'draft-7',
legacyHeaders: false,
});
app.post('/login', loginLimiter, loginHandler);
Apply stricter limits to login, password reset, sign-up and anything that sends email or SMS. If you run more than one instance, use a shared store (such as Redis) for the limiter.
7. Validate every input
Never trust req.body, req.query or req.params. Validate shape and types at the edge with a schema library such as Zod:
import { z } from 'zod';
const SignupSchema = z.object({
email: z.string().email().max(254),
password: z.string().min(12).max(128),
});
app.post('/signup', async (req, res) => {
const parsed = SignupSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: 'Invalid input' });
// use parsed.data from here on
});
For databases, use parameterised queries or an ORM such as Prisma - never build SQL with string concatenation. Our guide to Node.js, MySQL and Prisma shows the setup.
8. Hash passwords and protect sessions
- Hash passwords with bcrypt, scrypt or Argon2. Node's built-in
crypto.scryptworks without dependencies. - Compare secrets with
crypto.timingSafeEqual, not===. - Regenerate session IDs on login, expire sessions, and invalidate them on password change.
- Offer two-factor authentication for admin users.
9. Handle errors without leaking detail
Stack traces tell attackers about your code and dependencies. Return a generic message and log the detail server-side:
app.use((err, req, res, next) => {
console.error({ msg: err.message, stack: err.stack, path: req.path });
res.status(500).json({ error: 'Something went wrong' });
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection', reason);
process.exit(1); // let the platform restart the process cleanly
});
Set NODE_ENV=production, which also disables Express's verbose error pages.
10. Log carefully
Logs help you detect attacks, but they must not become a data leak. Log request IDs, user IDs, status codes and failed logins; never log passwords, tokens, full card numbers or ID numbers. This matters for POPIA too - see POPIA for developers.
11. Check your webhooks and outbound calls
- Verify incoming webhook signatures with HMAC before acting on them - see webhooks explained.
- Guard against server-side request forgery: if users can supply URLs your server fetches, allow-list hosts and block internal addresses.
- Set timeouts on every outbound HTTP call.
12. Choose hosting that does its share
| Responsibility | Managed app hosting | Your own VPS |
|---|---|---|
| OS and runtime patches | Host | You |
| TLS certificates | Host (automatic) | You |
| Firewall and network | Host | You |
| Backups | Host, plus your own copies | You |
| Your code and dependencies | You | You |
NewHost's Node.js hosting includes free SSL, encrypted environment variables, automatic backups and optional two-factor sign-in for your dashboard, so you can focus on the rows that are always yours.
The checklist at a glance
- Node.js 22 or 24 LTS, version pinned
-
npm ci, lockfile committed,npm auditin CI - Secrets in environment variables, none in Git
-
helmet(),x-powered-bydisabled - HTTPS only,
trust proxyset, secure cookies - Body size limit and rate limiting on sensitive routes
- Schema validation on all input, parameterised queries
- Strong password hashing, 2FA for admins
- Generic error responses, crash-and-restart on fatal errors
- No personal data or secrets in logs
- Webhooks verified, outbound calls time-limited
Frequently asked questions
Is Express secure enough for production?
Yes, when configured properly. Express is minimal by design, so security comes from the middleware and practices you add: headers, validation, rate limiting and careful error handling, as in this checklist.
How often should I update npm dependencies?
Review security advisories continuously through automated tools and apply critical fixes promptly. Batch routine minor updates weekly or fortnightly, and run your tests before deploying each batch.
Should I use the Node.js permission model?
Recent Node.js versions include a permission model that restricts file system and child process access. It can add defence in depth, but check the documentation for your exact version and test thoroughly before relying on it.
Do I need a web application firewall?
A WAF can help block common attack patterns, but it is not a substitute for validating input and patching dependencies. Get the basics in this checklist right first.
Ready to run your hardened app on South African servers? Explore Node.js hosting or compare plans.