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-fileflag in Node 20+).
Step-by-Step
- 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
HSTSheaders (see step 3). For local development, usehttpswith a self-signed certificate or a tool likemkcert. - Set secure HTTP headers. Use the
helmetmiddleware (for Express) or equivalent for your framework. It sets headers likeX-Content-Type-Options,X-Frame-Options, andReferrer-Policy. AddStrict-Transport-Security(HSTS) with a max-age of at least 15552000 seconds (180 days) andincludeSubDomains. - Validate and sanitize all input. Never trust query parameters, body payloads, or URL params. Use a validation library like
zod,joi, orexpress-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). - 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'. - 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-sanitizeto strip$and.from user input. - Mitigate brute-force attacks. Implement rate limiting per IP and per user. Use
express-rate-limitfor 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. - 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.
- Keep dependencies patched. Run
npm auditregularly and fix high-severity vulnerabilities. Usenpm outdatedto see what’s behind. Consider using a tool likeDependabotorRenovateto automate updates. Avoid installing packages you don’t need. - Use environment variables for secrets. Store API keys, database URLs, and JWT secrets in a
.envfile (never commit it) or in your deployment platform’s secret manager. Access them viaprocess.env. Never hardcode secrets in code. - 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
pinoorwinston. 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
validatororzod‘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.timingSafeEqualwhen comparing hashes or tokens to avoid timing attacks. - Run a security linter like
eslint-plugin-securityto catch common mistakes during development. - Regularly review your dependencies for known vulnerabilities using
npm auditand 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
- Python Integer Identity: Why It Varies by Run
- Can Technology Solve Poverty? Facts & Limits
- Wireless Controller Charging: Can You Play While Plugged In?
- Zoom Desktop Client: Features, Setup, and Fixes
- Cannot Detect Device? Fixes for USB & Bluetooth
- Super Cool Tech Backpack: The Future Is Here