keepcalculator

06 Numbers and Powers

Secure a Node.js API: Layered Checklist

Card No. 06·62187 · Updated · Standard formula, shown below

This guide walks you through a practical, layered approach to securing a Node.js API. You’ll move from foundational protections (HTTPS, headers) through authentication, input validation, and dependency hygiene, ending with logging and rate limiting. By the end, you’ll have a checklist you can apply to any Express, Fastify, or Koa API to close the most common security gaps.

What You’ll Need

  • A Node.js API project (Express, Fastify, or similar) in a code editor.
  • Node.js 18 or later installed locally.
  • Access to a terminal/command line.
  • Basic familiarity with npm or yarn.
  • If you plan to use environment variables, a package like dotenv (or Node’s built-in --env-file flag in Node 20+).

Step-by-Step

  1. Enable HTTPS in production. Never serve a live API over plain HTTP. Terminate TLS at a reverse proxy (Nginx, Caddy) or use a cloud load balancer. Redirect all HTTP traffic to HTTPS and set HSTS headers (see step 3). For local development, use https with a self-signed certificate or a tool like mkcert.
  2. Set secure HTTP headers. Use the helmet middleware (for Express) or equivalent for your framework. It sets headers like X-Content-Type-Options, X-Frame-Options, and Referrer-Policy. Add Strict-Transport-Security (HSTS) with a max-age of at least 15552000 seconds (180 days) and includeSubDomains.
  3. Validate and sanitize all input. Never trust query parameters, body payloads, or URL params. Use a validation library like zod, joi, or express-validator. Define schemas for every endpoint. Reject requests that don’t match the expected types, lengths, and patterns. Sanitize strings to strip HTML tags if you ever render them (e.g., in error messages).
  4. Implement authentication and authorization. For session-based auth, use secure, HttpOnly, SameSite cookies. For token-based auth, use short-lived JWTs (15 minutes) and refresh tokens stored in HttpOnly cookies. Never store tokens in localStorage. For authorization, use a middleware that checks the user’s role or permissions before allowing access to a route. For example, an admin-only route should check req.user.role === 'admin'.
  5. Prevent SQL/NoSQL injection. Use parameterized queries or an ORM/ODM that escapes input automatically (e.g., Sequelize, Mongoose with proper sanitization). Avoid string concatenation in queries. For MongoDB, use express-mongo-sanitize to strip $ and . from user input.
  6. Mitigate brute-force attacks. Implement rate limiting per IP and per user. Use express-rate-limit for general limits (e.g., 100 requests per 15 minutes per IP). For login endpoints, use a stricter limit (e.g., 5 attempts per 15 minutes). Consider adding a small delay after failed logins.
  7. Handle errors safely. Create a global error-handling middleware that logs the error stack to the server console but returns a generic message to the client (e.g., “Internal Server Error”). Never expose stack traces, database details, or internal paths in responses.
  8. Keep dependencies patched. Run npm audit regularly and fix high-severity vulnerabilities. Use npm outdated to see what’s behind. Consider using a tool like Dependabot or Renovate to automate updates. Avoid installing packages you don’t need.
  9. Use environment variables for secrets. Store API keys, database URLs, and JWT secrets in a .env file (never commit it) or in your deployment platform’s secret manager. Access them via process.env. Never hardcode secrets in code.
  10. Add logging and monitoring. Log all requests (method, path, status, duration) and security-relevant events (failed logins, validation errors). Use a structured logging library like pino or winston. In production, send logs to a centralized service (e.g., ELK, Datadog) and set up alerts for unusual patterns like a spike in 401 responses.

Common Mistakes

  • Skipping HTTPS in development. If you don’t test with HTTPS locally, you might miss mixed-content issues or cookie-related bugs that only appear in production.
  • Relying on CORS as a security boundary. CORS is a browser mechanism, not a server-side protection. An attacker can call your API from a non-browser tool. Always enforce authentication and input validation.
  • Storing JWTs in localStorage. This makes them accessible to any XSS script. Use HttpOnly cookies instead.
  • Overly permissive regex for email validation. A simple regex can reject valid emails or allow invalid ones. Use a library like validator or zod‘s built-in email type.
  • Logging sensitive data. Never log passwords, tokens, or full credit card numbers. Mask or redact them.

Tips

  • Use a layered approach: even if one layer fails, others should still protect you. For example, even if a route is accidentally unprotected, input validation and rate limiting can reduce damage.
  • Set a global request size limit (e.g., express.json({ limit: '10kb' })) to prevent large payload attacks.
  • Use crypto.timingSafeEqual when comparing hashes or tokens to avoid timing attacks.
  • Run a security linter like eslint-plugin-security to catch common mistakes during development.
  • Regularly review your dependencies for known vulnerabilities using npm audit and consider using a lockfile to pin versions.

Troubleshooting

Issue: After adding Helmet, your frontend can’t load images or styles from a CDN.
Fix: Helmet’s default Content-Security-Policy is strict. Adjust it via helmet({ contentSecurityPolicy: { directives: { ... } } }) to allow your CDN domains.

Issue: Rate limiting is blocking legitimate users.
Fix: Check your limits. For APIs, consider using a sliding window or a token bucket. Also, make sure you’re not counting static assets or health checks. Exclude them from the rate limiter.

Issue: JWT verification fails intermittently.
Fix: Check the clock skew between your server and the client. Use the clockTolerance option in jsonwebtoken (e.g., 30 seconds). Also, ensure the secret is the same across all instances.

Issue: Environment variables are undefined.
Fix: Make sure you load dotenv at the very top of your entry file. If using Node 20+, you can use node --env-file=.env server.js. Verify the .env file is in the correct directory and not gitignored by mistake.

Frequently Asked Questions

Should I use JWT or sessions for authentication?

For most APIs, short-lived JWTs with refresh tokens stored in HttpOnly cookies are a good balance. Sessions are simpler but require server-side storage and may not scale as easily.

Is it safe to disable CORS for my API?

If your API is only consumed by server-to-server calls or mobile apps, you can disable CORS. But if it’s used by a web frontend, you need CORS to allow your specific origin.

How often should I update dependencies?

At least monthly, and immediately for critical vulnerabilities. Use automated tools to stay on top of updates.

What is the best way to store secrets in Node.js?

Use environment variables in development and a secret manager (like AWS Secrets Manager or HashiCorp Vault) in production. Never commit secrets to version control.

Do I need to sanitize input if I use an ORM?

Yes. ORMs prevent SQL injection but not other attacks like XSS or command injection. Always validate and sanitize.

How can I test my API’s security?

Use tools like OWASP ZAP or Burp Suite to scan for common vulnerabilities. Also, write test cases for invalid input, unauthorized access, and rate limiting behavior.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →