DEV Community

Matt Rosenlund
Matt Rosenlund

Posted on

Introducing hapi-aegis: Helmet-style security headers for hapi.js

If you build APIs or apps with hapi, you've probably googled some variation of "hapi security headers" and landed on a mix of:

  • server.ext('onPreResponse', ...) snippets that set headers manually
  • blankie for CSP
  • README gists for HSTS, frameguard, referrer-policy, and the rest
  • Silent hope that nothing's been missed

Express has Helmet. One app.use(helmet()) and you get a sensible stack of security headers. Hapi has never had a direct equivalent — until now.

Meet hapi-aegis

hapi-aegis is a single plugin that applies sensible security-header defaults to every response your hapi server sends — including Boom error responses. It's Helmet, rebuilt natively for hapi.

const Hapi = require('@hapi/hapi');
const Aegis = require('hapi-aegis');

const server = Hapi.server({ port: 3000 });
await server.register(Aegis);
// That's it — every response now has a solid security-header baseline.
Enter fullscreen mode Exit fullscreen mode

curl -I the server and you'll see:

  • Content-Security-Policy
  • Strict-Transport-Security (HSTS)
  • X-Frame-Options
  • X-Content-Type-Options: nosniff
  • Referrer-Policy
  • Cross-Origin-Embedder-Policy, -Opener-Policy, -Resource-Policy
  • X-DNS-Prefetch-Control
  • Origin-Agent-Cluster
  • X-Permitted-Cross-Domain-Policies
  • X-XSS-Protection: 0 (the safe-by-default value)
  • X-Download-Options: noopen
  • Expect-CT (deprecated but included for legacy)
  • X-Powered-By and Server removed

15 middlewares total, each configurable or disableable independently.

Configuring what you need

Each middleware takes an options object. Here's HSTS with preload, a strict CSP, and the legacy XSS filter turned off entirely:

await server.register({
    plugin: Aegis,
    options: {
        hsts: {
            maxAge: 63072000,        // 2 years
            includeSubDomains: true,
            preload: true
        },
        contentSecurityPolicy: {
            useDefaults: false,
            directives: {
                defaultSrc: ["'self'"],
                scriptSrc: ["'self'"],
                styleSrc: ["'self'"],
                imgSrc: ["'self'"],
                connectSrc: ["'self'"],
                objectSrc: ["'none'"]
            }
        },
        xssFilter: false
    }
});
Enter fullscreen mode Exit fullscreen mode

Route-level overrides

If one endpoint needs different policy — a JSON-only API route with no CSP, or a public embed that allows framing — configure it on the route, not globally:

server.route({
    method: 'GET',
    path: '/api/data',
    options: {
        plugins: {
            aegis: {
                contentSecurityPolicy: false,   // skip CSP for this route
                frameguard: { action: 'deny' }  // but harden framing
            }
        }
    },
    handler: () => ({ ok: true })
});
Enter fullscreen mode Exit fullscreen mode

Route config is merged with server-level config; route wins per-middleware.

Boom errors get headers too

A subtle-but-important thing: if your app throws a Boom error (Boom.unauthorized(), Boom.badRequest(), etc.), the response that goes out is assembled by hapi's Boom handler, not your route's handler. A naive onPreResponse extension that only checks response.header() will silently skip error responses — your 401s and 500s ship without security headers.

hapi-aegis detects Boom responses (response.isBoom) and writes headers onto response.output.headers. A 400 gets the same CSP as a 200.

Works alongside existing hapi security plugins

If you already use something more specialized for a particular header, hapi-aegis is happy to step out of the way. For CSP specifically, blankie has features hapi-aegis doesn't — per-request nonce generation and dynamic per-request policy support. If you need those, use blankie for CSP and disable aegis's CSP:

await server.register({
    plugin: Aegis,
    options: {
        contentSecurityPolicy: false   // let blankie handle CSP
    }
});
Enter fullscreen mode Exit fullscreen mode

hapi-aegis still manages the other 14 headers.

TypeScript support

Full type definitions for the plugin and every middleware's options ship in the package (index.d.ts). No separate @types install.

Install

npm install hapi-aegis
Enter fullscreen mode Exit fullscreen mode

Feedback, issues, and PRs welcome. If you find a sensible security header I missed, open an issue — there are already a few on the roadmap (Permissions-Policy, Report-To, per-request CSP nonces).

Thanks for reading.

Top comments (0)