Actually, I saw another one of those “Top 10 Node.js Frameworks” lists floating around Twitter yesterday. You know the type — they list Express (which is basically a museum exhibit at this point), then throw in whatever flavor-of-the-month micro-framework is trending on GitHub, and maybe, if we’re lucky, Hapi gets a pity mention at number seven. It drives me nuts.
Well, that’s not entirely accurate. Here we are in 2026, and while everyone is chasing the latest edge-runtime-compatible, zero-config, AI-generated backend solution, I’m over here deploying Hapi.js. Again. And I’m sleeping like a baby because the thing just works. The team released v24.1.0 back in January, and honestly? It’s the most “boring” release I’ve seen in years. That is a massive compliment.
Stability isn’t sexy, but it pays the bills
I remember fighting with the early versions of Fastify and NestJS when they were breaking APIs every other Tuesday. Hapi has never done that to me. The philosophy hasn’t shifted: configuration over code, explicit over implicit.
The big news with the v24 cycle isn’t some “game-changing” AI integration. It’s that they finally, finally smoothed out the rough edges with native ESM (ECMAScript Modules) and top-level await support in the plugin ecosystem. But if you tried to mix CommonJS and ESM plugins in Hapi back in 2024, you know the pain. The dreaded ERR_REQUIRE_ESM haunted my dreams for months.
I tested the new upgrade path on a legacy client project last Tuesday — running Node 24.2.0 on my Linux workstation — and it was shockingly uneventful. No cryptic stack traces. It just booted.

The Code: Still Verbose, Still Safe
Critics always complain that Hapi is verbose. They aren’t wrong. You have to type more. But look at what you get for that extra typing: readability that doesn’t degrade six months later when you’ve forgotten how the app works.
import Hapi from '@hapi/hapi';
import Joi from 'joi';
const init = async () => {
const server = Hapi.server({
port: 3000,
host: 'localhost',
debug: { request: ['error'] } // Built-in logging is still a lifesaver
});
server.route({
method: 'POST',
path: '/news/subscribe',
options: {
validate: {
payload: Joi.object({
email: Joi.string().email().required(),
preferences: Joi.array().items(Joi.string()).min(1)
}),
failAction: (request, h, err) => {
// This lifecycle hook is where Hapi shines
console.error('Validation failed:', err.message);
throw err;
}
}
},
handler: async (request, h) => {
// If we get here, we KNOW the data is valid.
const { email } = request.payload;
// Simulate async db
Frequently asked questions
What's new in Hapi.js v24.1.0 released in January 2026?
Hapi v24.1.0, released in January 2026, is a stability-focused release rather than a feature overhaul. Its biggest improvement is smoothing out native ESM (ECMAScript Modules) and top-level await support across the plugin ecosystem. This finally resolves the long-standing pain of mixing CommonJS and ESM plugins, eliminating the dreaded ERR_REQUIRE_ESM errors that plagued developers back in 2024.
How do I fix ERR_REQUIRE_ESM when mixing CommonJS and ESM plugins in Hapi?
The fix is upgrading to Hapi v24, which finally smooths out native ESM and top-level await support in the plugin ecosystem. The author tested the upgrade path on a legacy client project running Node 24.2.0 on a Linux workstation and reported it was shockingly uneventful — no cryptic stack traces, it just booted cleanly without the ERR_REQUIRE_ESM errors that haunted developers in 2024.
Why is Hapi.js considered too verbose compared to other Node frameworks?
Critics are correct that Hapi requires more typing than alternatives, but that verbosity is intentional. It follows a philosophy of configuration over code and explicit over implicit. The payoff is readability that doesn't degrade six months later when you've forgotten how the app works. The extra typing buys long-term maintainability, which matters more than initial keystroke count on real projects.
How does Joi payload validation work with Hapi route options?
In Hapi, you define validation inside a route's options.validate.payload using a Joi schema — for example, Joi.object with email as Joi.string().email().required() and preferences as a Joi.array. A failAction lifecycle hook catches validation errors, logs them, and throws. Once the handler runs, you know the data is valid, so you can safely destructure fields like request.payload.email without defensive checks.
