Skip to content
NestJS ns security 4 min read

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

OptionPurposeTypical value
originWhich origins may read responsesarray, regex, or function
methodsAllowed HTTP verbs['GET','POST','PUT','DELETE']
allowedHeadersRequest headers the client may send['Content-Type','Authorization']
exposedHeadersResponse headers JS may read['X-Total-Count']
credentialsAllow cookies / Authorization on cross-site callstrue
maxAgeSeconds to cache the preflight (OPTIONS) result86400

When credentials: true, the spec forbids a wildcard origin. You must echo a specific origin, so always pass an explicit allow-list rather than origin: '*', 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 directives rather 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: true together with an explicit origin list — wildcards are rejected by the browser for credentialed requests.
  • Cap the preflight cache with maxAge to reduce OPTIONS chatter 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 -I after every deploy so a config regression cannot silently weaken your defences.
Last updated June 14, 2026
Was this helpful?