Skip to content
NewHost
Menu

Node.js Security Checklist - Securing Your App in Production

A practical Node.js security checklist for production - dependencies, secrets, headers, rate limiting, validation, auth, logging and hosting, with Express code.

By NewHost team · · 6 min read

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.json and install with npm ci in production builds, so what you tested is what you run.
  • Run npm audit --omit=dev in 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.scrypt works 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 audit in CI
  • Secrets in environment variables, none in Git
  • helmet(), x-powered-by disabled
  • HTTPS only, trust proxy set, 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.

Related guides

Ready to launch on NewHost?

Choose a plan and go live today, or tell us what you need and we'll recommend the right setup.