
hapi review
Configuration-driven Node.js server framework with built-in auth, validation, caching and plugins
Verdict
hapi suits teams that want auth, route validation, caching and a plugin system in one framework, configured per route, with official modules for JWT, cookies, third-party login and WebSockets. The catch is that it is kept going by a small volunteer committee on a modest community budget, and OpenAPI docs come only from the community hapi-swagger plugin.
Best for
Teams that want auth and validation configured per route in one framework
Not for
Teams that want OpenAPI from an official plugin or a large plugin ecosystem
Pros
- Auth schemes and strategies are part of the core
- Route validation for params, queries, payloads and headers with joi
- Ships its own TypeScript types
- Regular patch and security releases through 2026
Cons
- OpenAPI docs only through the community hapi-swagger plugin
- Run by volunteers on a small community budget
- Node.js minimums differ between hapi, its docs and joi
Is hapi right for your project?
Describe what you need and AI will check the fit against this review.
hapi features and capabilities
Built-in auth framework
Auth schemes and strategies are registered once and chosen per route, with official modules for JWT, cookies, basic auth and third-party login through bell.
Route validation
Each route can validate headers, params, query, payload and cookies with joi, and set a failAction for what happens when validation fails.
Plugins and server composition
Features are packaged as plugins with their own routes and settings, and glue builds a server from a manifest, which keeps larger apps split into parts.
Caching built in
Server methods and routes can cache results through catbox with memory, Redis or Memcached adapters.
WebSockets through nes
The official nes plugin adds a WebSocket adapter for hapi routes, so real-time features stay inside the same server and plugin model.
- TypeScript firstWritten in TypeScript or ships its own types, with typed routes or handlers
- YesShips types with typed route refs
- Request validationValidates bodies, params or queries against a schema, in the core or an official package
- YesRoute validation with joi from the hapijs org
- OpenAPI docsGenerates an OpenAPI spec or Swagger UI from routes
- Partlyhapi-swagger is a community plugin
- Beyond Node.jsOfficially runs on Bun, Deno or edge runtimes such as Cloudflare Workers
- NoNode.js only
- WebSocketsBuilt in or through an official plugin
- YesOfficial @hapi/nes plugin
- Project structureModules, dependency injection or a fixed layout, not only routing
- PartlyPlugins and glue manifests; no fixed layout
- Database layerAn official ORM, query builder or data layer
- No
- AuthenticationAn official auth module, plugin or strategy system
- YesCore auth strategies plus official jwt, cookie and bell
At a glance
- Type
- Configuration-driven server framework
- Stack
- JavaScript / Node.js 16+
Free vs paid editions
What you get when you self-host, and what needs a paid plan.
hapi is free and has no paid edition or official hosting. A volunteer committee maintains it, with funding through Open Collective.
Running hapi
hapi 21 declares Node.js 14.15 or later, but the current joi release needs Node.js 20, so plan for Node.js 20 or later.
New cloud server to ready
9 s
Memory in use at idle
81 MB of 4 GB
Reads per second
10,111
Writes per second
5,421
Startup
0.48 s
Version tested
21.4.12 · Oct 31, 2026
- The smallest install on disk, 36 packages and 5 MB with joi. It still took 9 seconds, longer than larger installs, so treat install times here as rough.
- By default hapi hides validation details and only says the payload is invalid. Passing the error through failAction shows joi's message, which names only the first invalid field.
- Reads had a median latency of 4.7 ms and 8.1 ms at the 99th percentile, and malformed JSON got a JSON error.
import Hapi from "@hapi/hapi";
import Joi from "joi";
// 100 items kept in memory, so the test measures the framework, not a database.
const items = new Map();
for (let i = 1; i <= 100; i++) items.set(i, { id: i, name: `Item ${i}`, price: i * 1.5, tags: ["sample"] });
let nextId = 101;
const server = Hapi.server({ port: Number(process.env.PORT ?? 3000), host: "127.0.0.1" });
server.route({ method: "GET", path: "/health", handler: () => ({ status: "ok" }) });
server.route({
method: "GET",
path: "/items/{id}",
handler: (req, h) => {
const item = items.get(Number(req.params.id));
if (!item) return h.response({ error: "Not found" }).code(404);
return item;
},
});
server.route({
method: "POST",
path: "/items",
options: {
validate: {
payload: Joi.object({ name: Joi.string().min(1).max(100).required(), price: Joi.number().min(0).required(), tags: Joi.array().items(Joi.string()).max(10) }),
// hapi hides validation details by default; return them like the other apps do.
failAction: (req, h, err) => { throw err; },
},
},
handler: (req, h) => {
const item = { id: nextId++, ...req.payload };
items.set(item.id, item);
return h.response(item).code(201);
},
});
await server.start();{
"statusCode": 400,
"error": "Bad Request",
"message": "\"name\" is not allowed to be empty",
"validation": {
"source": "payload",
"keys": [
"name"
]
}
}What you can do with hapi
APIs with several login methods
Serve one API to browsers with cookie sessions and to services with JWTs, choosing the strategy per route.
Validated internal services
Define each route's inputs with joi so bad requests are rejected before handler code runs.
Larger apps split into plugins
Let separate teams own plugins with their own routes and settings, composed into one server with glue.