Skip to content
ExponaBLOG
NESTJS · DAY 075 / 100

Helmet in NestJS: a practical guide for developers

Security headers in one line.

Expona Admin6 min read

Security headers in one line: why Helmet matters

Most teams delay security hardening until a vulnerability report lands in their inbox. Helmet in NestJS: a practical guide for developers exists precisely to prevent that scramble. By wrapping your Express or Fastify adapter with a single middleware, you automatically set dozens of HTTP security headers that defend against common attacks—clickjacking, MIME sniffing, XSS, and more—without writing custom header logic.

In production e-commerce or SaaS environments, we've seen teams lose hours tuning headers after launch. A few lines added to main.ts during initial setup saves that pain and gives you a solid security baseline from day one.

Getting started: app.use(helmet()) in main.ts

Helmet is the Express ecosystem standard for HTTP header security. NestJS integrates it seamlessly because NestJS wraps Express by default. Installation takes one command:

typescript
npm install helmet

Then import and apply it in your bootstrap function:

typescript
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();

That single line activates default protections:

  • Strict-Transport-Security: Forces HTTPS for the next 30 days (if your origin is HTTPS).
  • X-Content-Type-Options: Prevents browsers from sniffing MIME types, stopping certain polyglot attacks.
  • X-Frame-Options: Blocks framing of your app in iframes unless you explicitly allow it.
  • X-XSS-Protection: Legacy header; modern browsers ignore it, but older clients still benefit.
  • Referrer-Policy: Limits what referer information leaks to third-party sites.
  • Permissions-Policy: Controls which browser features (camera, microphone, geolocation) scripts may access.
  • Content-Security-Policy (CSP): Restricts where resources (scripts, styles, images) may load from.

In a real project, we deployed this to a ride-hailing backend serving both web and mobile clients. Within minutes, security scanners showed dramatic improvements without a single user-facing code change.

Configure Content Security Policy for HTML-serving applications

If your NestJS app serves Server-Side Rendered (SSR) HTML or has inline scripts, the default CSP is too strict. Helmet's default CSP blocks all inline scripts and styles, which breaks many applications.

Here's how to customize it for a typical SaaS dashboard:

typescript
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({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "https://trusted-cdn.example.com"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"],
        connectSrc: ["'self'", "https://api.example.com"],
        fontSrc: ["'self'", "https://fonts.googleapis.com"],
        frameSrc: ["'none'"],
      },
    },
  }));
  
  await app.listen(3000);
}

bootstrap();

Breaking down this configuration:

  • defaultSrc: Fallback policy. Only load from the same origin.
  • scriptSrc: Scripts from your origin or a specific trusted CDN. Remove 'unsafe-inline' unless absolutely necessary.
  • styleSrc: 'unsafe-inline' is sometimes unavoidable for CSS-in-JS libraries; we prefer bundled CSS.
  • imgSrc: Allow your origin, data URIs (for small inlined images), and HTTPS sources.
  • connectSrc: Only fetch/XHR/WebSocket to your origin or API domain.
  • frameSrc: Block embedding of iframes entirely, or list specific origins if you embed payment forms or widgets.

In a travel booking system we worked on, we initially set scriptSrc: ["'self'"] but forgot our analytics script. The browser silently blocked it, causing confusion until we checked the CSP violations report. Build a logging endpoint for CSP violations and monitor it in production:

typescript
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
    },
    reportUri: '/api/csp-report',
  },
}));

Then in your controller, log and act on violations to catch misconfigurations early.

Using @fastify/helmet for the Fastify adapter

If you're using NestJS with Fastify instead of Express, you need @fastify/helmet instead of the generic helmet package:

typescript
npm install @fastify/helmet

Register it as a plugin in your Fastify setup:

typescript
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(
    AppModule,
    new FastifyAdapter(),
  );
  
  await app.register(helmet);
  
  await app.listen(3000);
}

bootstrap();

The API is nearly identical. Customization works the same way:

typescript
await app.register(helmet, {
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
    },
  },
});

We chose Fastify for a high-throughput CRM backend handling 50k requests per minute. Helmet integration was effortless, and performance overhead was negligible.

Check your results with securityheaders.com

After deploying Helmet, verify that headers are actually being set and graded by external auditors. securityheaders.com is the standard tool:

  1. Navigate to https://securityheaders.com in your browser.
  2. Enter your production domain.
  3. Wait for the scan to complete.
  4. Review the report, which grades you on letter scale (A to F).

A typical Helmet-enabled site scores A or A+. The report shows exactly which headers are present and their values. You'll see:

  • Missing headers (sometimes expected, e.g., if you don't use CSP reporting).
  • Weak configurations (e.g., CSP using too many 'unsafe-inline' directives).
  • Suggestions for improvement.

In a previous e-commerce project, the security team insisted on X-Frame-Options: DENY, which broke an embedded payment iframe. The securityheaders report made it obvious we needed ALLOW-FROM or to reconsider architecture. The report is also useful for stakeholders who want proof of security posture.

Browser DevTools also show headers:

typescript
// Inspect in Chrome DevTools > Network > select any request > Response Headers
strict-transport-security: max-age=15552000; includeSubDomains
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: no-referrer
permissions-policy: camera=(), microphone=(), geolocation=()

Common mistakes and how to avoid them

Forgetting to apply Helmet before other middleware: Always call app.use(helmet()) early in your bootstrap, before route handlers or custom middleware that might conflict. Helmet should be one of the first lines after NestFactory.create().

Over-permissive CSP: A CSP that allows 'unsafe-inline' for scripts negates much of the protection. We often see teams disable CSP entirely because "it breaks things." Instead, invest an hour auditing your dependencies. Bundled CSS and externally-sourced scripts are the better path.

Not testing in staging: Helm defaults can break authentication redirects, third-party embeds, or analytics. Always deploy to a staging environment first and run your integration test suite. Check CSP violation logs.

Ignoring HSTS for non-HTTPS origins: If your app doesn't yet run on HTTPS, Helmet's HSTS header doesn't apply anyway. But once you enable HTTPS, that header persists in browsers for 30 days by default. Plan for this; don't flip HTTP off and on repeatedly in staging.

Hardcoding CSP without environment variables: Your staging domain, API endpoint, and CDN differ from production. Use environment variables or a config file so CSP directives match your deployment:

typescript
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: [
        "'self'",
        process.env.SCRIPT_SRC_DOMAIN || 'https://cdn.example.com',
      ],
      connectSrc: ["'self'", process.env.API_BASE_URL || 'https://api.example.com'],
    },
  },
}));

Key takeaways

  • Install helmet (or @fastify/helmet for Fastify) and call app.use(helmet()) in your main.ts bootstrap function to enable HTTP security headers automatically.
  • For HTML-serving apps, customize the CSP directives to allow your legitimate scripts, styles, and API endpoints; avoid 'unsafe-inline' for scripts where possible.
  • Test your configuration in staging first, monitoring CSP violation logs to catch misconfigurations.
  • Use securityheaders.com to verify your headers are set correctly and receive a security grade for your domain.
  • Review common mistakes—forgetting to apply Helmet early, over-permissive policies, and hardcoded domain values—before deploying to production.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles