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
- What You’ll Build
- Prepare Your Project: Stack & Structure
- Environment Variables and Storing Secrets
- Set Up MongoDB and Connect With Mongoose
- Sending Emails in Node.js
- Load the User’s Profile Picture From Gravatar Using Virtuals in Mongoose
- Implement a User Profile Editing Screen
- Generate a Username in Mongoose Middleware
- Displaying Seasons and Episodes for TV Shows with Mongoose Relationship Population
- Implementing Pagination for Movies
- Implement a Watchlist
- Create a Full Text Search with MongoDB
- Create a REST API with JSON Endpoints
- Update Mongoose Models for JSON Responses
- API Pagination for TV Shows
- Customize API Endpoints with Query Parameters
- Always Throw and Handle API Validation Errors
- Advanced API Validation With Custom Errors
- Create an API Documentation with Swagger
- Customize Your Swagger API Documentation URL
- Describe Endpoint Details in Your Swagger API Documentation
- 10 Tips on API Testing With Postman
- JWT Authentication in Swagger API Documentation
- API Versioning with Request Headers
- API Login With Username and Password to Generate a JWT
- JWT Authentication and Private API Endpoints
- Refresh Tokens With JWT Authentication
- Create a JWT Utility
- JWT Refresh Token for Multiple Devices
- Check Refresh Token in Authentication Strategy
- Rate Limit Your Refresh Token API Endpoint
- How to Revoke a JWT
- Invalidate JWTs With Blacklists
- JWT Logout (Part 1/2)
- JWT “Immediate” Logout (Part 2/2)
- A Better Place to Invalidate Tokens
- How to Switch the JWT Signing Algorithm
- Roll Your Own Refresh Token Authentication Scheme
- JWT Claims 101
- Use JWT With Asymmetric Signatures (RS256 & Co.)
- Encrypt the JWT Payload (The Simple Way)
- Increase JWT Security Beyond the Signature
- Unsigned JSON Web Tokens (Unsecured JWS)
- JWK and JWKS Overview
- Provide a JWKS API Endpoint
- Create a JWK from a Shared Secret
- JWT Verification via JWKS API Endpoint
- What is JOSE in JWT
- Encrypt a JWT (the JWE Way)
- Authenticate Encrypted JWTs (JWE)
- Encrypted and Signed JWT (Nested JWT)
- Bringing Back JWT Decoding and Authentication
- Bringing Back JWT Claims in the JWT Payload
- How to Run Separate Frontend and Backend Servers Within a Single Project
- How to Use Server Labels
- How to Correctly Stop Your Server and Close Existing Connections
- Basic Authentication With Username and Password
- Authentication and Remember Me Using Cookies
- How to Set a Default Authentication Strategy
- Define Multiple Authentication Strategies for a Route
- Restrict User Access With Scopes
- Show „Insufficient Scope“ View for Routes With Restricted Access
- Access Restriction With Dynamic and Advanced Scopes
- hapi - How to Fix „unknown authentication strategy“
- Authenticate with GitHub And Remember the Login
- Authenticate with GitLab And Remember the User
- How to Combine Bell With Another Authentication Strategy
- Custom OAuth Bell Strategy to Connect With any Server
- Redirect to Previous Page After Login
- How to Implement a Complete Sign Up Flow With Email and Password
- How to Implement a Complete Login Flow
- Implement a Password-Reset Flow
- Views in hapi 9 (and above)
- How to Render and Reply Views
- How to Reply and Render Pug Views (Using Pug 2.0)
- How to Create a Dynamic Handlebars Layout Template
- Create and Use Handlebars Partial Views
- Create and Use Custom Handlebars Helpers
- Specify a Different Handlebars Layout for a Specific View
- How to Create Jade-Like Layout Blocks in Handlebars
- Use Vue.js Mustache Tags in Handlebars Templates
- How to Use Multiple Handlebars Layouts
- How to Access and Handle Request Payload
- Access Request Headers
- How to Manage Cookies and HTTP States Across Requests
- Detect and Get the Client IP Address
- How to Upload Files
- Quick Access to Logged In User in Route Handlers
- How to Fix “handler method did not return a value, a promise, or throw an error”
- How to Fix “X must return an error, a takeover response, or a continue signal”
- Query Parameter Validation With Joi
- Path Parameter Validation With Joi
- Request Payload Validation With Joi
- Validate Query and Path Parameters, Payload and Headers All at Once on Your Routes
- Validate Request Headers With Joi
- Reply Custom View for Failed Validations
- Handle Failed Validations and Show Errors Details at Inputs
- How to Fix AssertionError, Cannot validate HEAD or GET requests
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
- hapi server connection configuration options
- vision package for view support in hapi
9.x.x
- lodash: utilities for Node.js/io.js