Introduction
Beyond authenticating individual users, an Express application needs baseline hardening against common web attack vectors. Three widely used tools address different layers of this: Helmet sets secure HTTP response headers to reduce risks like clickjacking and MIME-sniffing attacks, CORS (Cross-Origin Resource Sharing) middleware controls which external domains are allowed to call your API from a browser, and rate limiting protects against brute-force login attempts, credential stuffing, and denial-of-service style abuse by capping how many requests a client can make in a given time window.
Cricket analogy: Just as a stadium needs perimeter security, pitch inspectors, and crowd-control gates as separate layers, Helmet, CORS, and rate limiting each guard a different entry point into your Express app rather than one guard covering everything.
Syntax
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const app = express();
app.use(helmet());
app.use(cors({
origin: ['https://myapp.com', 'https://admin.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true
}));
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 login attempts per window
message: { error: 'Too many login attempts, please try again later' }
});
app.post('/login', loginLimiter, loginHandler);Explanation
helmet() applies a collection of small middleware functions that set headers such as Content-Security-Policy, X-Content-Type-Options: nosniff, X-Frame-Options, and Strict-Transport-Security, each closing off a specific class of attack (e.g., preventing the browser from being tricked into rendering your site inside a malicious iframe). The cors package controls the Access-Control-Allow-Origin header and related headers; without it, browsers block cross-origin requests by default, and misconfiguring it with origin: '*' alongside credentials: true can expose authenticated endpoints to any website. express-rate-limit tracks request counts per client (by IP by default) within a rolling time window and returns a 429 Too Many Requests response once the limit is exceeded — applying a strict limiter specifically to authentication routes is a key defense against brute-force password guessing.
Cricket analogy: helmet() is like a fielding coach setting multiple field restrictions at once (headers) to stop specific shots, while cors is the boundary rope deciding which spectators can walk onto the field, and express-rate-limit is the over-limit rule capping how many deliveries a bowler can send.
Example
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const app = express();
app.use(helmet());
app.use(express.json());
app.use(cors({ origin: process.env.ALLOWED_ORIGIN, credentials: true }));
const apiLimiter = rateLimit({ windowMs: 60 * 1000, max: 100 });
app.use('/api', apiLimiter);
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 });
app.post('/api/login', authLimiter, (req, res) => {
// ...authenticate user...
res.json({ message: 'Login attempt processed' });
});
app.listen(process.env.PORT, () => console.log('Server running securely'));Output
With this setup, every response carries Helmet's protective headers automatically. A request from an origin not listed in ALLOWED_ORIGIN is blocked by the browser with a CORS error before it can read the response. A client that submits more than 5 login attempts within 15 minutes receives HTTP 429 with the JSON body { "error": "Too many login attempts, please try again later" } instead of being allowed to keep guessing passwords, while normal API traffic under the /api prefix is still allowed up to 100 requests per minute.
Cricket analogy: Once fielded, every ball bowled carries the umpire's standard checks automatically; a spectator from a non-accredited section trying to reach the pitch is turned away at the rope, and a bowler who oversteps the no-ball line five times in an over is flagged and benched rather than allowed to keep bowling.
Never hardcode secrets — JWT signing keys, database credentials, API keys, or SMTP passwords — directly in source code or commit them to version control; load them from environment variables (e.g., process.env.JWT_SECRET) via a .env file that is excluded with .gitignore. In production, always serve the app over HTTPS so that tokens, cookies, and credentials are encrypted in transit and cannot be intercepted by attackers on the network.
Key Takeaways
- helmet() sets protective HTTP headers with a single line, guarding against clickjacking, MIME sniffing, and related attacks.
- Configure CORS with an explicit allow-list of trusted origins rather than '*', especially when credentials (cookies/tokens) are involved.
- Apply stricter rate limits to sensitive routes like /login and /signup to blunt brute-force and credential-stuffing attacks.
- Combine these with input validation, HTTPS, and hashed passwords for defense in depth — no single layer is sufficient alone.
Practice what you learned
1. What is the primary purpose of the helmet middleware in an Express app?
2. Why is combining origin: '*' with credentials: true in a CORS configuration considered dangerous?
3. What HTTP status code does express-rate-limit typically return once a client exceeds the configured request limit?
4. Why is it good practice to apply a stricter rate limiter specifically to a /login route rather than relying only on a general API-wide limiter?
Was this page helpful?
You May Also Like
Authentication Basics and JWT
Learn how authentication works in web APIs and how JSON Web Tokens (JWT) provide a stateless way to verify user identity.
Middleware in Express
Master the Express middleware chain, the next() function, and the special error-handling middleware signature.
Environment Variables and Configuration
Learn how to manage configuration and secrets in Node.js apps using environment variables and dotenv.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics