Shipping an Express app to production without security hardening is like leaving your front door open with a sign that says "help yourself." The good news is that the Node.js ecosystem has excellent, battle-tested tools that handle most of the heavy lifting for you. This lesson walks you through the four pillars of Express production security.
Security headers with HelmetWhat is helmet?An Express middleware package that sets recommended HTTP security headers (CSP, HSTS, X-Frame-Options) in one line.
HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. headers control how browsers behave when they load your app. Left at their defaults, browsers make permissive choices that attackers can exploit, things like loading scripts from any domain or allowing your pages to be embedded in iframes on malicious sites. Helmet sets safe defaults for all of these in one line.
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
hsts: {
maxAge: 31536000, // 1 year in seconds
includeSubDomains: true,
preload: true
}
}));<script> tag pointing to their own server, CSP stops the browser from running it.Key headers Helmet sets
| Header | What it does |
|---|---|
X-Frame-Options | Prevents your site from being embedded in iframes (clickjacking) |
X-Content-Type-Options | Stops browsers from guessing file types (MIME sniffing) |
Strict-Transport-Security | Forces HTTPS for the configured duration |
Content-Security-Policy | Allowlists trusted sources for scripts, styles, images |
Referrer-Policy | Controls how much URL info leaks to other sites |
Secure CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. configuration
CORS (Cross-Origin Resource Sharing) controls which other websites can make requests to your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. from a user's browser. Calling app.use(cors()) with no arguments opens your API to every originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443. on the internet, any website could read your users' data through their browsers.
import cors from 'cors';
const whitelist = ['https://my-app.com', 'https://admin.my-app.com'];
const corsOptions = {
origin: (origin, callback) => {
if (whitelist.includes(origin) || !origin) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization']
};
app.use(cors(corsOptions));The !origin check allows server-to-server requests (like from Postman or curl) which don't send an Origin header. In a more strict environment you might want to remove that.
Rate limitingWhat is rate limiting?Restricting how many requests a client can make within a time window. Prevents brute-force attacks and protects your API from being overwhelmed.
Think of rate limiting like a bouncer at the door. Each IP addressWhat is ip address?A numerical label (e.g., 172.217.14.206) that identifies a device on a network - DNS translates domain names into IP addresses. gets a limited number of attempts within a time window, and once they hit the limit, they wait outside. This stops bots from hammering your login endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. thousands of times a second trying to guess passwords.
import rateLimit from 'express-rate-limit';
// General limit for all routes
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15-minute window
max: 100,
message: 'Too many requests from this IP, please try again later'
});
app.use(generalLimiter);
// Stricter limit for authentication endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true // Only count failed attempts
});
app.use('/auth/', authLimiter);Input validation and XSSWhat is xss?Cross-Site Scripting - an attack where malicious JavaScript is injected into a web page and runs in other users' browsers, stealing data or hijacking sessions. protection
Your database should never see raw, unvalidated user input. Two libraries cover the most common attack vectors: NoSQLWhat is nosql?A category of databases that store data without fixed table schemas, using documents, key-value pairs, or graphs instead of rows and columns. injection and cross-site scripting (XSS).
import mongoSanitize from 'express-mongo-sanitize';
import xss from 'xss-clean';
// Strip MongoDB operator characters from user input
app.use(mongoSanitize());
// Clean HTML tags from user input to prevent XSS
app.use(xss());For more structured validation, use zod or joi to define exactly what shape your data should be before your route handlers ever touch it. Reject anything that doesn't fit the schemaWhat is schema?A formal definition of the structure your data must follow - which fields exist, what types they have, and which are required. with a 400 error.
Quick reference
| Risk | Defense | Package |
|---|---|---|
| Clickjacking, MIME sniffing | Security headers | helmet |
| Cross-origin data theft | CORS whitelist | cors |
| Brute force, DoS | Rate limiting | express-rate-limit |
| NoSQL injection | Input sanitization | express-mongo-sanitize |
| XSS attacks | HTML cleaning | xss-clean |
| Schema violations | Input validation | zod / joi |