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.
curl -I the server and you'll see:
Content-Security-Policy-
Strict-Transport-Security(HSTS) X-Frame-OptionsX-Content-Type-Options: nosniffReferrer-Policy-
Cross-Origin-Embedder-Policy,-Opener-Policy,-Resource-Policy X-DNS-Prefetch-ControlOrigin-Agent-ClusterX-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-ByandServerremoved
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
}
});
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 })
});
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
}
});
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
- npm: https://www.npmjs.com/package/hapi-aegis
- GitHub: https://github.com/mrosenlund/hapi-aegis
- Docs: full API reference, CSP deep-dive, and comparison tables in the README.
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)