100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Node.js

Securing Express Apps (Helmet, CORS, Rate Limiting)

Harden Express applications with secure HTTP headers, controlled cross-origin access, and rate limiting to mitigate common attacks.

Authentication & SecurityIntermediate12 min readJul 8, 2026
Analogies

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

javascript
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

javascript
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

Was this page helpful?

Topics covered

#NodeJs#NodeJsExpressStudyNotes#WebDevelopment#SecuringExpressAppsHelmetCORSRateLimiting#Securing#Express#Apps#Helmet#StudyNotes#SkillVeris