12-factor app config, Helmet security headers, express-rate-limit, CORS, HTTPS enforcement, secure cookie flags, and secrets management patterns.
Module P-6 — Configuration, Security Hardening, and Rate Limiting
What this module covers: An API that works on your machine is not a production API. This module covers the 12-factor approach to configuration so secrets never end up in source control, the HTTP security headers that close the most common attack vectors, CORS configured correctly so browsers can reach your API, rate limiting that stops brute-force attacks, and the secure cookie flags that protect tokens from XSS. These are not optional polish — they are the baseline that separates hobby projects from production systems.
12-Factor Configuration: No Secrets in Code
The Twelve-Factor App methodology's rule on configuration: store config in the environment, never in the code. Every value that changes between environments (dev, staging, production) is configuration. Every credential is configuration.
dotenv for local development
Load dotenv once, at the very start of your application:
Config validation with Zod
Raw process.env is untyped — every value is string | undefined. Validate it at startup so the app fails fast with a clear error instead of silently failing later:
Import env instead of process.env everywhere:
Helmet: HTTP Security Headers
Helmet sets HTTP response headers that tell browsers how to handle your content safely. It prevents a class of attacks that have nothing to do with your application logic.
That one line sets these headers (among others):
| Header | What it does |
|---|---|
Content-Security-Policy | Restricts which resources the browser can load — blocks inline script injection |
X-Frame-Options | Prevents clickjacking (your page can't be embedded in an iframe) |
X-Content-Type-Options: nosniff | Prevents MIME type sniffing — browser uses declared content type |
Strict-Transport-Security | Forces HTTPS for future requests (HSTS) |
Referrer-Policy | Controls how much URL info is sent in the Referer header |
X-Permitted-Cross-Domain-Policies | Blocks Adobe Flash/Acrobat cross-domain requests |
For APIs returning JSON, you can relax the CSP since there is no HTML to protect:
CORS: Cross-Origin Resource Sharing
Browsers block cross-origin requests by default. CORS is the mechanism that lets your API tell browsers which origins are allowed.
The wrong way — a development shortcut that leaks into production:
The right way — explicit allow list:
In .env:
Preflight requests: Before sending a non-simple request (POST with JSON, any DELETE, any custom header), browsers send an OPTIONS preflight to ask permission. Helmet and the cors package handle this automatically. If you see OPTIONS requests failing, check that your CORS config allows the method and headers being used.
Rate Limiting
Without rate limiting, a single client can flood your auth endpoints with thousands of login attempts per second. Rate limiting is the first line of defense against brute force, credential stuffing, and denial-of-service.
Global rate limit — apply to all routes:
Stricter limits for auth endpoints:
Apply globally first, then stricter limits on specific routes:
Rate limiting with Redis (for multi-server deployments):
The default in-memory store does not share state across processes. If you have two app servers, each has its own counter — a client gets 2× the allowed requests. Use Redis for a shared store:
Secure Cookie Flags
When storing refresh tokens in HTTP-only cookies (recommended over response body), these flags are non-negotiable:
Install cookie-parser to read cookies:
| Flag | What it prevents |
|---|---|
httpOnly | XSS — attacker's injected script can't read the token |
secure | Token transmitted over cleartext HTTP |
sameSite: strict | CSRF — browser doesn't send the cookie on cross-site requests |
path: '/auth' | Token sent on every request, not just auth endpoints |
Additional Security Practices
Disable the X-Powered-By header — don't advertise your stack:
Sanitise MongoDB/NoSQL injection (if using MongoDB):
Request size limits — prevent large payloads from exhausting memory:
HTTPS in production — your app should run behind a TLS-terminating reverse proxy (nginx, Cloudflare, AWS ALB). The app itself typically serves HTTP internally. If you need HTTPS at the app level:
Putting It Together: Security Middleware Stack
The order matters — helmet and rate limiters should run before routes:
Summary
- 12-factor config: all env-specific values in environment variables, validated with Zod at startup, never in code.
- Helmet: one line that sets a dozen security headers — Content-Security-Policy, X-Frame-Options, HSTS, and more. Always use it.
- CORS: explicit origin allow list from environment variables. Never
cors()with no options in production. - Rate limiting: global limiter for all routes, stricter limiter on auth endpoints. Use Redis store when running multiple servers.
- HTTP-only cookies: store refresh tokens in
httpOnly; Secure; SameSite=Strictcookies with a scoped path — the trifecta that blocks XSS, HTTPS downgrade, and CSRF. - Request size limits:
express.json({ limit: '10kb' })prevents memory exhaustion from large payloads.
Next: connecting external services — Redis caching with the cache-aside pattern, sending email, uploading files to object storage, and making outbound HTTP requests to third-party APIs.
What is the Twelve-Factor App methodology's rule regarding configuration management?
Why is it dangerous to simply use app.use(cors()) without passing any options in a production environment?
When storing a refresh token in a cookie, which flag is responsible for ensuring that malicious JavaScript (XSS) cannot read the token?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
Sign in & RegisterDiscussion
0Join the discussion