hapi — How to Run Separate Frontend and Backend Servers Within a Single Project

Building web apps require you to build a frontend for user interaction and a backend for data processing. Often, you’ll put both ends into one project and define routes which render web views.

At some point, you’ll want to add an API for mobile apps or third-party integrations. And with that, you need to expose additional API routes using HTTP verbs to create, read, update or delete documents. Now the project and complexity kind of messed up, because you’re exposing web routes and API routes from within the same server and need to handle them internally in complete separation.

Using hapi as your framework of choice offers the capability to create and run separate server connections from within one project. That means you can create the frontend server, add only the frontend routes, and register additional plugins. The same holds true for the API server.

We’ll guide you through the setup of multiple servers within one hapi project and show you how to configure each server separately.

hapi Series Overview

Why Should I Do That?

Actually, the split servers for frontend and API is just a preference, not a recommendation. We think it’s a good structure to separate the logic for frontend and backend. Often, the frontend uses different strategies for authentication than the backend. You’re possibly using a cookie based strategy within your frontend and a token based strategy for the API.

This way, you still have both *ends within your project. You can keep working on both sides when necessary and the context switch is likely smaller than two completely separated projects. Additionally, this approach reduces the complexity of two independent, isolated projects and deployments. Small scale setups will reuse core functionality within the backend to process data before returning the response via API or within a rendered view.

Start Multiple Hapi Servers Within One Project

You need a hapi server instance to define multiple incoming connections where the server should listen for requests. The following code shows the basic server definition including the two connection configurations to listen on ports 3000 and 3001 for requests.

Precisely, there is only one actual hapi server integrating two logical servers. The method to configure a server connection returns a server object. This server object for a given connection configuration can be customized with specific routes, plugins, functionality, and many more.

var hapi = require('hapi');  
var port = 3000;  
var _ = require('lodash');

// Create hapi server instance
var server = new hapi.Server();

// add connection parameters
server.connection({  
    host: 'localhost',
    port: process.env.PORT || port
});

server.connection({  
    host: 'localhost',
    port: process.env.PORT + 1 || port + 1
});

// Start the server
server.start(function () {  
    // Log to the console the host and port info
    _.forEach(server.connections, function(connection) {
        console.log('Server started at: ' + connection.info.uri);
    });
});

If you want to run this code example above, initialize a new node project with npm init and install the hapi and lodash packages:

mkdir hapi-multiple-servers && cd hapi-multiple-servers  
npm install hapi lodash  
nano index.js  

Now copy and paste the contents above into your index.js file and save it. Ready to rumble? Start the server and you’ll see the logs that your server started on ports 3000 and 3001.

$ node index.js
Server started at: http://0.0.0.0:3000  
Server started at: http://0.0.0.0:3001  

Within the code snippet above, we create a new hapi server instance with new hapi.Server() and add two connection configurations for incoming requests. Both connections listen on localhost and different ports. We use lodash to simplify the iteration through the server connections and just log the uri information for each connection.

Frontend and Backend Server in One Project

The example above shows you how to define multiple connection configurations where your server will be listening for requests. Now we will add different routes, register plugins and define a template engine for the split server connections.

Hapi’s server.connection([options]) method returns a server object with the new connection configuration defined. The options object is cloned deeply so you’re not gonna override them with definitions for other connections. Make sure you only use values that can be cloned deeply.

Let’s jump into the code. We’re going to explain the parts of frontend and backend below the snippet.

var hapi = require('hapi');  
var _ = require('lodash');  
var port = 3000;

// Create hapi server instance
var server = new hapi.Server();


/**
 * Frontend Server configuration
 **/

// add frontend connection parameters
var frontend = server.connection({  
    host: 'localhost',
    port: process.env.PORT || port,
    labels: 'frontend'
});

// add routes
frontend.route({  
    method: 'GET',
    path: '/',
    handler: function (request, reply) {
        return reply('Hello without HTML view');
    }
});

// register hapi-auth-cookie plugin
frontend.register(require('hapi-auth-cookie'), function (err) {  
    if (err) {
        throw err;
    }

    frontend.auth.strategy('session', 'cookie', {
        password: 'secretpassword',
        cookie: 'cookie-name',
        redirectTo: '/login',
        isSecure: false
    });
});

frontend.register(require('vision'), function (err) {  
    if (err) {
        throw err;
    }

    // register view template engine
    server.views({
        engines: {
            html: require('handlebars')
        }
    });
});

/**
 * Backend Server configuration
 **/

// add frontend connection parameters
var backend = server.connection({  
    host: 'localhost',
    port: process.env.PORT + 1 || port + 1,
    labels: 'backend'
});


// add backend-only route
backend.route({  
    method: 'GET',
    path: '/status',
    handler: function (request, reply) {
        return reply('ok');
    }
});

// register hapi-auth-jwt plugin only for backend
backend.register({  
    register: require('hapi-auth-jwt')
}, function (err) {
    if (err) {
        throw err;
    }

    backend.auth.strategy('token', 'jwt', {
        key: 'supersecretkey',
        validateFunc: function (decodedToken, callback) {
          console.log('jwt validate function')
        }
    });
});


// Start the server
server.start(function () {  
    // Log to the console the host and port info
    _.forEach(server.connections, function(connection) {
        console.log('Server started at: ' + connection.info.uri);
    });
});

We’ve tried to squeeze the snippet as much as we could to keep it understandable. Even though it’s kind of extensive, it show the capabilities defining multiple server connections.

First, we create a hapi server instance and afterwards add the connection settings for the frontend. Since the hapi connection method returns a new server object, we can do further configurations as we desire. We register handlebars as our template engine, add a route to the index / and register the hapi-auth-cookie plugin for the server. Since we only use cookies within the frontend, there is no need to also drag it into the backend.

For the backend, we configure a port offset of 1 respective the frontend to avoid the EADDRINUSE error. It’s not possible to listen on the same UDP/TCP ports for different servers. Afterwards, we add a single route /status and register the hapi-auth-jwt plugin. There is no need to configure a template engine, because we won’t render any views and respond in plain text or JSON.

That’s it, you just need to install the plugins and afterwards replace the content of your index.js file (if you created it before). Starting the server will log the same messages as before with the difference, that you’ve a clear split between your frontend and backend server.

npm install vision hapi-auth-jwt hapi-auth-cookie

$ node index.js
Server started at: http://localhost:3000  
Server started at: http://localhost:3001  

There Is a Dark Side

You can’t deploy changes to either frontend or backend without accepting a downtime for both. Internally, hapi handles the logical frontend and backend servers encapsulated within an overall server object. When starting the node process to kick off the hapi server, it automatically boots up the frontend and backend. Making changes to either of them will result in a restart of the overall hapi server and implies a downtime for both.

Conclusion

This articles shows you how to set up two servers within a single hapi project. Of course you can add further connection configurations to spin up more servers and listen on additional ports for incoming requests.

Defining two servers for different tasks allows you to slim down the functionality of either of them. You can focus on just the needed functionality, plugins, and routes for each of them and implement just what’s required.


Additional Resources

Explore the Library

Find interesting tutorials and solutions for your problems.