Helmet & CORS
Two of the cheapest, highest-impact security wins in any HTTP API are setting sane response headers and locking down which origins may call you. Helmet stamps your responses with hardening headers — a Content Security Policy, HSTS, frame protection, and more — that close off whole classes of browser-side attacks. CORS, meanwhile, decides which web origins the browser will let read your responses. NestJS exposes first-class hooks for both, but the wiring differs between the default Express adapter and Fastify, so this page covers each.
Applying Helmet for security headers
Helmet is a collection of small middleware functions that set protective HTTP headers. On the default Express platform you register it globally in main.ts after creating the app. Install it first:
npm install helmet
// main.ts
import { NestFactory } from '@nestjs/core';
import helmet from 'helmet';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(helmet());
await app.listen(3000);
}
bootstrap();
With defaults enabled, a response now carries a hardened header set. You can confirm with a quick request:
curl -sI http://localhost:3000/ | grep -iE 'content-security-policy|strict-transport|x-'
Output:
Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;...
Strict-Transport-Security: max-age=15552000; includeSubGroups
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Frame-Options: SAMEORIGIN
Register Helmet before any route or other middleware so its headers apply to every response, including error responses produced by exception filters.
Tuning CSP and HSTS
The Content Security Policy is the header most likely to need customisation, because a too-strict policy blocks legitimate scripts, styles, or images. Pass a config object to override individual directives instead of disabling the whole policy:
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'https://cdn.jsdelivr.net'],
imgSrc: ["'self'", 'data:', 'https://images.example.com'],
},
},
hsts: {
maxAge: 31536000, // one year, in seconds
includeSubDomains: true,
preload: true,
},
}),
);
If you serve a pure JSON API with no browser-rendered HTML, you can safely disable CSP (contentSecurityPolicy: false) since there is no document for the browser to attack — but keep the other headers on.
Configuring CORS
CORS controls which origins a browser allows to read cross-site responses. NestJS proxies to the underlying cors package, so you can enable it inline or with a typed options object. The simplest form allows every origin, which is fine for public read-only APIs but unsafe when you use cookies.
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400, // cache preflight for 24h
});
origin accepts a boolean, a string, an array, a regex, or a function for dynamic allow-lists driven by config:
const allowed = new Set(['https://app.example.com', 'https://staging.example.com']);
app.enableCors({
origin: (origin, callback) => {
if (!origin || allowed.has(origin)) return callback(null, true);
callback(new Error('Origin not allowed by CORS'));
},
credentials: true,
});
Common CORS options
| Option | Purpose | Typical value |
|---|---|---|
origin | Which origins may read responses | array, regex, or function |
methods | Allowed HTTP verbs | ['GET','POST','PUT','DELETE'] |
allowedHeaders | Request headers the client may send | ['Content-Type','Authorization'] |
exposedHeaders | Response headers JS may read | ['X-Total-Count'] |
credentials | Allow cookies / Authorization on cross-site calls | true |
maxAge | Seconds to cache the preflight (OPTIONS) result | 86400 |
When
credentials: true, the spec forbids a wildcard origin. You must echo a specific origin, so always pass an explicit allow-list rather thanorigin: '*', or the browser will silently drop the response.
Platform differences for Fastify
Fastify uses dedicated plugins rather than the Express middleware packages. Register them on the underlying instance via app.register(...):
npm install @fastify/helmet @fastify/cors
// main.ts (Fastify)
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import helmet from '@fastify/helmet';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.register(helmet, {
contentSecurityPolicy: {
directives: { defaultSrc: ["'self'"] },
},
});
// enableCors still works on Fastify and maps to @fastify/cors internally
app.enableCors({
origin: ['https://app.example.com'],
credentials: true,
});
await app.listen(3000, '0.0.0.0');
}
bootstrap();
Note that app.enableCors() works on both adapters — Nest routes it to the correct implementation — but Helmet must be registered with the platform-specific package. Mixing the Express helmet middleware into a Fastify app will not apply.
Best Practices
- Register Helmet first in
bootstrap()so every response, including errors, gets hardened headers. - Customise the CSP
directivesrather than disabling CSP wholesale; only turn it off for pure non-HTML JSON APIs. - Drive CORS allow-lists from configuration, never a hardcoded
'*'when cookies are in play. - Set
credentials: truetogether with an explicitoriginlist — wildcards are rejected by the browser for credentialed requests. - Cap the preflight cache with
maxAgeto reduceOPTIONSchatter without making policy changes slow to take effect. - Use the platform-specific packages (
@fastify/helmet,@fastify/cors) when running on Fastify; do not import the Express middleware. - Verify your headers in CI or with
curl -Iafter every deploy so a config regression cannot silently weaken your defences.