Express Cheatsheet
CORS and Security
Use this Express reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
CORS — Cross-Origin Resource Sharing
CORS headers tell browsers whether cross-origin requests are allowed. Without them, same-origin policy blocks frontend JavaScript from calling your API.
cors Package (Recommended)
npm install cors
const cors = require('cors'); // Allow all origins (development only — never in production) app.use(cors()); // Specific origin app.use(cors({ origin: 'https://app.example.com' })); // Multiple origins const allowed = ['https://app.example.com', 'https://admin.example.com']; app.use(cors({ origin: (origin, callback) => { if (!origin || allowed.includes(origin)) return callback(null, true); callback(new Error('Not allowed by CORS')); }, })); // Full options app.use(cors({ origin: 'https://app.example.com', methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], exposedHeaders: ['X-Total-Count', 'X-Request-Id'], credentials: true, // allow cookies / auth headers maxAge: 86400, // preflight cache (seconds) preflightContinue: false, // pass OPTIONS to next handler if true optionsSuccessStatus: 204, // some legacy browsers choke on 204 }));
Enabling Credentials
If the client sends cookies or Authorization headers, both sides must opt in:
// Server app.use(cors({ origin: 'https://app.example.com', credentials: true }));
// Client (fetch) fetch('https://api.example.com/data', { credentials: 'include' });
credentials: trueis incompatible withorigin: '*'— you must specify an exact origin.
Per-Route CORS
const corsAll = cors(); const corsRestrict = cors({ origin: 'https://partner.com' }); app.get('/public', corsAll, handler); app.get('/partner', corsRestrict, handler);
Manual CORS Headers (without package)
app.use((req, res, next) => { res.set('Access-Control-Allow-Origin', 'https://app.example.com'); res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.set('Access-Control-Allow-Credentials', 'true'); if (req.method === 'OPTIONS') return res.sendStatus(204); // handle preflight next(); });
CORS Header Reference
| Header | Direction | Purpose |
|---|---|---|
Access-Control-Allow-Origin | Response | Allowed origin(s) |
Access-Control-Allow-Methods | Response (preflight) | Allowed methods |
Access-Control-Allow-Headers | Response (preflight) | Allowed request headers |
Access-Control-Expose-Headers | Response | Headers visible to JS |
Access-Control-Allow-Credentials | Response | Allow cookies/auth |
Access-Control-Max-Age | Response (preflight) | Preflight cache duration |
Origin | Request | Origin of the request |
Helmet — Security Headers
npm install helmet
const helmet = require('helmet'); // Sensible defaults — use this first, then customize app.use(helmet());
What helmet() Sets by Default (helmet v7+)
| Header | Default Value |
|---|---|
Content-Security-Policy | Strict policy blocking inline scripts, etc. |
Cross-Origin-Opener-Policy | same-origin |
Cross-Origin-Resource-Policy | same-origin |
Origin-Agent-Cluster | ?1 |
Referrer-Policy | no-referrer |
Strict-Transport-Security | max-age=31536000; includeSubDomains (180 days before helmet v8) |
X-Content-Type-Options | nosniff |
X-DNS-Prefetch-Control | off |
X-Download-Options | noopen |
X-Frame-Options | SAMEORIGIN |
X-Permitted-Cross-Domain-Policies | none |
X-XSS-Protection | 0 (disables the buggy legacy filter — CSP is the real defense) |
helmet() also removes X-Powered-By. Since helmet v7 (2023), Cross-Origin-Embedder-Policy is not set by default (it broke embedding third-party resources); opt in with helmet({ crossOriginEmbedderPolicy: true }).
Customizing Helmet
app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", 'https://cdn.example.com'], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:', 'https:'], connectSrc: ["'self'", 'https://api.example.com'], }, }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true, }, frameguard: { action: 'deny' }, // block all framing referrerPolicy: { policy: 'same-origin' }, })); // Disable a specific protection app.use(helmet({ contentSecurityPolicy: false }));
Individual Helmet Middleware
app.use(helmet.contentSecurityPolicy({ directives: { /* ... */ } })); app.use(helmet.hsts({ maxAge: 31536000 })); app.use(helmet.noSniff()); app.use(helmet.frameguard({ action: 'sameorigin' })); app.use(helmet.xssFilter()); // only sets X-XSS-Protection: 0 (disables the legacy filter) app.use(helmet.hidePoweredBy()); // same as app.disable('x-powered-by')
Rate Limiting
npm install express-rate-limit
const rateLimit = require('express-rate-limit'); // Global limiter const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes limit: 100, // requests per window per IP ('max' is the legacy pre-v7 alias) standardHeaders: true, // Return rate limit info in `RateLimit-*` headers legacyHeaders: false, // Disable `X-RateLimit-*` headers message: { error: 'Too many requests, please try again later.' }, }); app.use('/api', limiter); // Tighter limit for auth endpoints const authLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minute limit: 5, message: { error: 'Too many login attempts.' }, skipSuccessfulRequests: true, // don't count 2xx responses }); app.post('/api/auth/login', authLimiter, loginHandler);
Hiding Express Fingerprint
app.disable('x-powered-by'); // Or with helmet: app.use(helmet.hidePoweredBy());
Input Sanitization
npm install express-mongo-sanitize # NoSQL injection (Express 4 only — see note) npm install xss # XSS in string values
const mongoSanitize = require('express-mongo-sanitize'); const xss = require('xss'); // Strip $ and . from keys (prevents NoSQL injection) — Express 4 app.use(mongoSanitize()); // Sanitize user strings before rendering const safe = xss(req.body.comment);
Express 5 incompatibility:
express-mongo-sanitizethrows at request time on Express 5 becausereq.queryis now a read-only getter it can't reassign. On v5, sanitizereq.bodyyourself (strip$/.-prefixed keys) or validate shapes with a schema library (zod,joi) instead of mutating the request.
HTTP Parameter Pollution
npm install hpp
const hpp = require('hpp'); app.use(hpp()); // keeps last value of duplicate query params, whitelist options available
Security Checklist
| Concern | Solution |
|---|---|
| CORS misconfiguration | Whitelist exact origins; never * with credentials |
| Missing security headers | helmet() |
| Brute-force auth | express-rate-limit on /auth/* |
| Large payloads (DoS) | express.json({ limit: '10kb' }) |
| XSS via reflected input | Sanitize with xss; set CSP |
| NoSQL injection | express-mongo-sanitize |
| SQL injection | Parameterized queries / ORM |
| Path traversal | path.basename(), resolve + check prefix |
| Clickjacking | helmet.frameguard({ action: 'deny' }) |
| MIME sniffing | helmet.noSniff() |
| Information leakage | Disable x-powered-by, generic error messages in prod |
| HTTPS enforcement | helmet.hsts(), redirect HTTP → HTTPS |
| Cookie security | httpOnly: true, secure: true, sameSite: 'strict' |