Security Middleware
Harden Express APIs with helmet for security headers, cors for cross-origin policy, and rate limiting for abuse prevention.
Search across all documentation pages
Harden Express APIs with helmet for security headers, cors for cross-origin policy, and rate limiting for abuse prevention.
Quick-reference recipe card - copy-paste ready.
import express from "express";
import helmet from "helmet";
import cors from "cors";
import rateLimit from "express-rate-limit";
const app = express();
app.set("trust proxy", 1);
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGIN ?? "https://app.example.com",
credentials: true,
}));
app.use(express.json({ limit: "1mb" }));
app.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
}));When to reach for this: Every public-facing API. Security middleware is not optional for production endpoints.
import express from "express";
import helmet from "helmet";
import cors from "cors";
import rateLimit from "express-rate-limit";
const app = express();
app.set("trust proxy", 1);
app.use(helmet({
contentSecurityPolicy: false, // enable if serving HTML from Express
crossOriginResourcePolicy: { policy: "cross-origin" },
}));
const allowedOrigins = ["https://app.example.com", "https://staging.example.com"];
app.use(cors({
origin(origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
credentials: true,
}));
const apiLimiter = rateLimit({
windowMs: 60_000,
max: 60,
standardHeaders: true,
legacyHeaders: false,
});
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
message: { error: "Too many login attempts" },
});
app.use(express.json({ limit: "1mb" }));
app.use("/api", apiLimiter);
app.post("/api/auth/login", authLimiter, loginHandler);
app.get("/health", (_req, res) => res.json({ ok: true }));
function loginHandler(_req: express.Request, res: express.Response) {
res.json({ token: "..." });
}What this demonstrates:
helmet sets security headers (X-Content-Type-Options, HSTS, etc.)Access-Control-* headers; browsers enforce CORS, server-to-server calls ignore it| Header | Set by | Purpose |
|---|---|---|
Strict-Transport-Security | helmet | Force HTTPS |
X-Content-Type-Options | helmet | Prevent MIME sniffing |
Access-Control-Allow-Origin | cors | Allowed browser origin |
RateLimit-Remaining | rate-limit | Client-visible quota |
import { RedisStore } from "rate-limit-redis";
import { createClient } from "redis";
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const limiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
windowMs: 60_000,
max: 100,
});origin: * with credentials - browsers reject this combination. Fix: specify exact origins./auth/login.| Alternative | Use When | Don't Use When |
|---|---|---|
| API gateway rate limiting | Kong, AWS API Gateway in front | Simple single-service deploy |
| Cloudflare WAF | DDoS and bot protection at edge | Internal-only API |
Fastify @fastify/helmet | Fastify stack | Express codebase |
NestJS ThrottlerModule | NestJS with decorator limits | Plain Express |
Native mobile apps do not enforce CORS. You still need auth. CORS matters for browser-based clients (SPA, admin dashboard).
Yes. Security headers protect against unexpected content-type attacks and improve security scanner scores.
Start with 100 requests/minute per IP for general API, 10/15min for login. Tune based on real traffic patterns.
Mount webhook routes before the limiter or use a skip function that checks the path.
Yes. The cors package responds to OPTIONS automatically when configured.
CORS does not prevent CSRF. Use csurf or SameSite cookies with double-submit tokens for cookie sessions.
Yes via @fastify/helmet. Same concepts apply.
curl -sI https://api.example.com/health and verify Strict-Transport-Security, X-Content-Type-Options, etc.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 18, 2026