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
