Security Best Practices
Securing a Node API means layering defenses — secure HTTP headers with Helmet, controlled cross-origin access with CORS, rate limiting, input validation, and keeping secrets out of your code — so common attacks are blocked before they reach your logic.
Learn Security Best Practices in our free Node.js course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Node.js course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
In this lesson you'll see what Helmet's headers do, how to allowlist origins with CORS, how rate limiting throttles abuse, why you must validate and sanitize input, how to keep secrets in environment variables, and a tour of the OWASP basics that tie it all together.
What You'll Learn in This Lesson
1️⃣ Secure Headers (Helmet)
Browsers honour special HTTP response headers that harden a site. Helmet sets a sensible bundle of them in one line. Key ones: X-Content-Type-Options: nosniff stops MIME-sniffing, X-Frame-Options: DENY blocks clickjacking, Strict-Transport-Security forces HTTPS, and a Content-Security-Policy restricts where resources may load from. The model below builds that same header set.
2️⃣ Helmet, CORS & Rate Limiting in Express
In a real app these are three middlewares you add early. helmet() sets the headers above; cors() controls which origins may call your API — always allowlist specific origins instead of '*' ; and express-rate-limit caps requests per IP so brute-force and abuse hit a 429 . Capping the JSON body size adds one more guard.
3️⃣ How Rate Limiting Works
Rate limiting is easiest to understand as a token bucket : each client starts with N tokens, every request spends one, and when the bucket is empty further requests are rejected with 429 Too Many Requests until it refills. The runnable model below gives each client 3 tokens, so the 4th request onward is blocked — exactly what express-rate-limit does per IP.
Your turn. The sanitizer below works once you fill in the single blank marked ___ . Follow the 👉 hint, then run it.
Mini-Challenge: Validate a Signup
No blanks — just a brief and an outline. Write input validation that rejects a bad email and a too-short password with clear error messages. Build it, run it, and compare with the example output.
Common Errors (and the fix)
- ❌ Hardcoding secrets in code — keys committed to git leak forever. ✅ Load them from process.env and keep .env in .gitignore .
- ❌ CORS set to '*' on an authenticated API — any site can call it. ✅ Allowlist your own origins.
- ❌ Trusting client input — unchecked data enables injection and XSS. ✅ Validate types/shape and sanitize before storing or rendering.
- ❌ No rate limiting on login — invites brute-force attacks. ✅ Add express-rate-limit , with a shared store across instances.
- ❌ Building SQL with string concatenation — classic SQL injection. ✅ Use parameterized queries or an ORM that escapes inputs for you.
Pro Tips
- 💡 Defense in depth: no single layer is enough — combine headers, CORS, limits, validation, and secret management.
- 💡 Validate with a schema library: tools like Zod or Joi make input rules declarative and reusable.
- 💡 Keep dependencies patched: run npm audit and update regularly — most breaches exploit known, fixed vulnerabilities.
📋 Quick Reference — Security Layers
Concern
Tool
Secure headers
app.use(helmet())
Origin control
cors({ origin: [...] })
Throttle abuse
express-rate-limit
Validate input
Zod / Joi schema
Secrets
process.env (.env)
SQL safety
parameterized queries
Audit deps
npm audit
Frequently Asked Questions
🎉 Lesson Complete!
- ✅ Helmet sets secure headers (nosniff, frame options, HSTS, CSP) in one line
- ✅ CORS should allowlist specific origins, never blanket '*' on authenticated APIs
- ✅ Rate limiting caps requests per client and returns 429 when exceeded
- ✅ Always validate and sanitize input — never trust the client
- ✅ Keep secrets in environment variables, out of source control
- ✅ Defense in depth plus npm audit covers the OWASP basics
- ✅ Next lesson: Building CLI Tools — ship command-line apps
Practice quiz
What does the Helmet middleware do?
- Encrypts the database
- Sets secure HTTP response headers
- Rate limits requests
- Validates input schemas
Answer: Sets secure HTTP response headers. Helmet sets a bundle of security-related response headers in one line.
Which header value stops browsers from MIME-sniffing responses?
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- Strict-Transport-Security
- Content-Security-Policy
Answer: X-Content-Type-Options: nosniff. X-Content-Type-Options: nosniff tells browsers not to guess the MIME type.
Why shouldn't you set CORS to allow all origins ('*') on an authenticated API?
- It is slower
- It breaks JSON parsing
- Any site could trigger requests on behalf of a logged-in user
- It disables HTTPS
Answer: Any site could trigger requests on behalf of a logged-in user. A wildcard origin lets any website call your API, dangerous for cookie/session-based auth.
What HTTP status does a rate limiter return once the cap is exceeded?
- 403 Forbidden
- 401 Unauthorized
- 500 Server Error
- 429 Too Many Requests
Answer: 429 Too Many Requests. Rate limiting returns 429 Too Many Requests when a client exceeds its allowance.
Where should secrets like API keys and DB passwords live?
- In environment variables, not in source code
- Hardcoded in the main file
- In a public README
- In the git commit message
Answer: In environment variables, not in source code. Load secrets from process.env (e.g. a gitignored .env) so they never get committed.
What attack does rate limiting most directly help blunt?
- MIME sniffing
- Brute-force password guessing
- Clickjacking
- CSS injection
Answer: Brute-force password guessing. Capping requests per client limits how many password attempts an attacker can make.
Which approach prevents classic SQL injection?
- String concatenation of inputs
- Logging the query
- Parameterized queries or an ORM that escapes inputs
- Disabling the database
Answer: Parameterized queries or an ORM that escapes inputs. Parameterized queries keep user input as data, never as executable SQL.
What does the express-rate-limit 'max' option set?
- The response timeout
- The body size limit
- The number of CORS origins
- The number of requests per IP per window
Answer: The number of requests per IP per window. max caps how many requests a single IP may make within windowMs before getting a 429.
What should you do with untrusted client input before storing or rendering it?
- Validate and sanitize it
- Trust it for speed
- Email it to yourself
- Cache it in Redis
Answer: Validate and sanitize it. Always validate types and shape and sanitize input to defend against injection and XSS.
What does the X-Frame-Options: DENY header protect against?
- SQL injection
- Clickjacking via iframes
- Slow database queries
- Expired TLS certificates
Answer: Clickjacking via iframes. Denying framing prevents your pages being embedded in a malicious iframe (clickjacking).
Continue this course
- Previous: Caching with Redis
- Next: Building CLI Tools