← Back to DevBytes

Building Full-Stack Apps with Hapi

Building Full-Stack Apps with Hapi

What is Hapi?

Hapi (short for "HTTP API") is a rich framework for building applications and services in Node.js. Unlike Express, which provides minimal abstraction, Hapi comes with a powerful plugin system, built-in input validation, caching, authentication, and a configuration-driven design. It was originally developed by Walmart Labs to handle high-traffic e-commerce systems and has since become a trusted choice for enterprise-grade full-stack applications.

Why Hapi Matters for Full-Stack Development

When building a full-stack application, you need a backend that can handle complex routing, data validation, authentication, and seamless integration with front-end technologies. Hapi offers:

Setting Up a Hapi Project

Start with a new Node.js project and install Hapi:

mkdir hapi-fullstack
cd hapi-fullstack
npm init -y
npm install @hapi/hapi

Create a simple server file, server.js:

'use strict';

const Hapi = require('@hapi/hapi');

const init = async () => {
    const server = Hapi.server({
        port: 3000,
        host: 'localhost'
    });

    server.route({
        method: 'GET',
        path: '/',
        handler: (request, h) => {
            return 'Hello, Full-Stack Hapi!';
        }
    });

    await server.start();
    console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
    console.log(err);
    process.exit(1);
});

init();

Run with node server.js and visit http://localhost:3000.

Building the Backend API with Hapi

Hapi routes are objects with a method, path, handler, and optional options for validation and authentication. Use @hapi/joi for input validation:

npm install @hapi/joi

Example REST endpoints for a simple task manager:

const Joi = require('@hapi/joi');

let tasks = [];

// CREATE a task
server.route({
    method: 'POST',
    path: '/tasks',
    options: {
        validate: {
            payload: Joi.object({
                title: Joi.string().min(1).max(100).required(),
                completed: Joi.boolean().default(false)
            })
        }
    },
    handler: (request, h) => {
        const task = {
            id: tasks.length + 1,
            title: request.payload.title,
            completed: request.payload.completed
        };
        tasks.push(task);
        return h.response(task).code(201);
    }
});

// READ all tasks
server.route({
    method: 'GET',
    path: '/tasks',
    handler: (request, h) => {
        return tasks;
    }
});

// READ a single task
server.route({
    method: 'GET',
    path: '/tasks/{id}',
    options: {
        validate: {
            params: Joi.object({
                id: Joi.number().integer().min(1).required()
            })
        }
    },
    handler: (request, h) => {
        const task = tasks.find(t => t.id === request.params.id);
        if (!task) {
            return h.response({ error: 'Task not found' }).code(404);
        }
        return task;
    }
});

Integrating a Frontend (Static Files and Templates)

Hapi can serve static files (HTML, CSS, JS) and render server-side views. Install the inert plugin for static files and vision for template engines:

npm install @hapi/inert @hapi/vision handlebars

Register plugins and configure Handlebars views:

await server.register([
    require('@hapi/inert'),
    require('@hapi/vision')
]);

server.views({
    engines: {
        html: require('handlebars')
    },
    relativeTo: __dirname,
    path: 'views',
    layout: true,
    layoutPath: 'views/layouts'
});

Create a views folder with index.html:

<!DOCTYPE html>
<html>
<head>
    <title>Task Manager</title>
</head>
<body>
    <h1>Tasks</h1>
    <ul>
        {{#each tasks}}
            <li>{{title}} - {{#if completed}}Done{{else}}Pending{{/if}}</li>
        {{/each}}
    </ul>
    <script src="/public/app.js"></script>
</body>
</html>

Serve static assets from a public folder:

server.route({
    method: 'GET',
    path: '/public/{param*}',
    handler: {
        directory: {
            path: 'public'
        }
    }
});

Now the server can render a page that lists tasks:

server.route({
    method: 'GET',
    path: '/',
    handler: (request, h) => {
        return h.view('index', { tasks });
    }
});

Connecting to a Database (MongoDB Example)

For persistence, integrate MongoDB via Mongoose:

npm install mongoose

Connect in server.js:

const mongoose = require('mongoose');

await mongoose.connect('mongodb://localhost:27017/hapi-tasks', {
    useNewUrlParser: true,
    useUnifiedTopology: true
});

Define a Task model:

const taskSchema = new mongoose.Schema({
    title: { type: String, required: true, maxlength: 100 },
    completed: { type: Boolean, default: false }
});
const Task = mongoose.model('Task', taskSchema);

Update the POST route to save to MongoDB:

handler: async (request, h) => {
    const task = new Task({
        title: request.payload.title,
        completed: request.payload.completed
    });
    const saved = await task.save();
    return h.response(saved).code(201);
}

Authentication and Authorization

Hapi makes authentication easy with plugins like hapi-auth-jwt2. Install it:

npm install hapi-auth-jwt2 jsonwebtoken

Register the authentication scheme and strategy:

const Jwt = require('@hapi/jwt'); // or hapi-auth-jwt2
const jwt = require('jsonwebtoken');

await server.register(require('hapi-auth-jwt2'));

server.auth.strategy('jwt', 'jwt', {
    key: 'your-secret-key',
    validate: async (decoded, request) => {
        // Check if user exists in database
        const user = await User.findById(decoded.id);
        if (!user) {
            return { isValid: false };
        }
        return { isValid: true, credentials: { id: user.id, scope: user.role } };
    },
    verifyOptions: { algorithms: ['HS256'] }
});

server.auth.default('jwt'); // protect all routes by default

Create a login route that issues tokens:

server.route({
    method: 'POST',
    path: '/login',
    options: { auth: false }, // public
    handler: async (request, h) => {
        const { email, password } = request.payload;
        // validate user credentials (simplified)
        const user = await User.findOne({ email });
        if (!user || user.password !== password) {
            return h.response({ error: 'Invalid credentials' }).code(401);
        }
        const token = jwt.sign({ id: user.id }, 'your-secret-key', { expiresIn: '1h' });
        return { token };
    }
});

Best Practices

Conclusion

Hapi provides a robust, opinionated foundation for building full-stack applications with Node.js. Its plugin system, built-in validation, and support for both server-side rendering and REST APIs make it a versatile choice for projects of any scale. By following the patterns outlined in this tutorialβ€”setting up a server, defining validated routes, integrating a front-end, connecting a database, and securing endpoints with JWTβ€”you can build production-ready full-stack apps that are maintainable, secure, and performant. Whether you are creating a small prototype or a large enterprise application, Hapi's configuration-driven approach will help you keep your code clean and your logic well-organized.

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles