Configurable web and API server powered by HAPI for the Okanjo App ecosystem.
This package bundles all the common things needed to build a web or API server, such as:
- Run a HTTP/API server (via hapi)
- Provides a consistent way for apps to define routes
- Serve static assets (via inert)
- Render template views (via vision and nunjucks)
- Handle JSONP requests and error responses consistently
- Report bad request responses for dev/production debugging
- Run a WebSocket server (via socket.io)
- Being totally configurable.
Setup is done mostly through configuration. Using all of these modules together requires a fair amount of boilerplate. This module attempts to eliminate most of the boilerplate setup with a reusable, configurable module, so your app can development time can focus on building the app, not boilerplate.
You should have a basic understanding of how HAPI works, otherwise this module won't make a ton of sense to you.
Add to your project like so:
npm install okanjo-app-server
Note: requires the
okanjo-app
module.
- Updated to Hapi v20
- Updated Socket.io to v4.4
- Updated Okanjo-App to v3
- Updated to Hapi v18+
Here's a super basic implementation.
Your directory structure might look like this:
example-app/
routes/
– place to put your route filesexample-routes.js
– example route file, seen below
static/
– place to put your static assets like css, images, js, etcview-extensions/
– place to stick nunjucks extensionsexample-ext.js
– example extension file, seen below
views/
– place to put your view templatesexample.j2
– example template, seen below
config.js
– okanjo-app configindex.js
– app entrypoint
You can find these example files here: docs/example-app
"use strict";
const Path = require('path');
module.exports = {
webServer: {
// Hapi server / global settings
hapiServerOptions: {
// Listening port
port: 3000, // Port to listen on, default: null (os assigned)
}, // HAPI server settings, see: // https://hapijs.com/api#server()
// Graceful shutdown handling
drainTime: 5000, // how long to wait to drain connections before killing the socket, in milliseconds, default: 5000
// Route configuration
routePath: Path.join(__dirname, 'routes'), // where to find route files, default: undefined
// Socket.io configuration
webSocketEnabled: true, // Whether to enable socket.io server, default: false
webSocketConfig: undefined, // socket.io server options, see: https://socket.io/docs/server-api/#new-server-httpserver-options (default: undefined)
// View handler configuration
viewHandlerEnabled: true, // Whether to enable template rendering, default: false
viewPath: Path.join(__dirname, 'views'), // The directory where view files are based from, required if viewHandlerEnabled is enabled.
cacheTemplates: false, // Whether to let hapi-vision cache templates for better performance, default: false
nunjucksEnvOptions: undefined, // http://mozilla.github.io/nunjucks/api.html#configure - e.g. { noCache: true }
nunjucksExtensionsPath: Path.join(__dirname, 'view-extensions'), // The directory where extension modules live, sig: function(env) { /* this = webServer */ }
// Static file handler configuration
staticHandlerEnabled: true, // Whether to enable static asset serving, default: false
staticPaths: [ // Array of path to route definitions for arbitrary paths, default: []
{ path: Path.join(__dirname, 'static'), routePrefix: '/' }, // exports the static/ directory under /
{ path: Path.join(__dirname, 'dist'), routePrefix: '/dist' } // exports the dist/ directory under /dist
],
staticListingEnabled: false, // Whether to allow directory listings, default: false
staticNpmModules: [ // Array of module names and paths to expose as static paths, useful for exposing dependencies on the frontend w/o build tools, default: []
{ moduleName: 'async', path: 'dist' } // e.g. node_modules/async/dist/async.min.js -> /vendor/async/async.min.js
]
}
};
This config.js
includes all available options. You may exclude or comment-out the ones that do not apply to your application.
"use strict";
const OkanjoApp = require('okanjo-app');
const OkanjoServer = require('okanjo-app-server');
// Configure the app
const config = require('./config.js');
const app = new OkanjoApp(config);
// Configure the server
const server = new OkanjoServer(app, app.config.webServer);
// Start it up
(async () => {
await server.init(); // optional, if you wish to do your own setup before starting HAPI
await server.start();
})()
.then(() => {
console.log('Server started at:', server.hapi.info.uri);
console.log('Use Control-C to quit')
})
.catch((err) => {
console.error('Something went horribly wrong', err);
process.exit(1);
})
;
You can make this much more elaborate by starting the server in a worker using okanjo-app-broker so you can hot-reload the entire server on changes, etc.
A route file needs to export a function. The context of the function (this
) will be the OkanjoServer instance.
Route files are loaded synchronously, so no async operations should be performed.
"use strict";
/**
* @this OkanjoServer
*/
module.exports = function() {
// This route replies with a rendered view using the example.j2 template and given context
this.hapi.route({
method: 'GET',
path: '/',
handler: (request, h) => {
return h.view('example.j2', {
boom: "roasted"
});
},
config: {
// ... validation, authentication. tagging, etc
}
});
// This route replies with an api response
this.hapi.route({
method: 'GET',
path: '/api/sometimes/works',
handler: async (request, h) => {
const res = await pretendServiceFunction(); // Fire off a pretend service function
return this.app.response.ok(res); // Return the response
},
config: {
// ... validation, authentication. tagging, etc
}
});
/**
* Pretend service function that returns a payload or throws an error
*/
const pretendServiceFunction = async () => {
if (Math.random() >= 0.50) { // half the time, return an error
throw this.app.response.badRequest('Nope, not ready yet.');
} else {
return { all: 'good' };
}
};
};
A Nunjucks extension file needs to export a function. The context of the function (this
) will be the OkanjoServer instance.
Nunjucks extension files are loaded synchronously, so no async operations should be performed.
"use strict";
/**
* @this OkanjoServer
* @param env – Nunjucks environment
*/
module.exports = function(env) {
// Remember, this.app is available here :)
// You could add globals to Nunjucks
env.addGlobal('env', this.app.currentEnvironment);
env.addGlobal('pid', process.pid);
// You could add custom filters to Nunjucks
env.addFilter('doSomething', (str, count) => {
// return some string
return "yay fun " + str + " " + count;
});
};
Views are standard Nunjucks templates. For example:
<html>
<head>
<link rel="stylesheet" href="/css/example.css" />
</head>
<body>
<ul>
<li>Boom: {{boom}}</li><!-- Set by routes/example-routes.js's GET / route -->
<li>ENV: {{env}}</li><!-- Set by view-extensions/example-ext.js -->
<li>PID: {{pid}}</li><!-- Set by view-extensions/example-ext.js -->
<li>doSomething: {{ boom|doSomething(1) }}</li><!-- Custom filter defined by view-extensions/example-ext.js -->
</ul>
</body>
</html>
The template, when rendered via http://localhost:3000/
shows:
<html>
<head>
<link rel="stylesheet" href="/css/example.css" />
</head>
<body>
<ul>
<li>Boom: roasted</li><!-- Set by routes/example-routes.js's GET / route -->
<li>ENV: default</li><!-- Set by view-extensions/example-ext.js -->
<li>PID: 2875</li><!-- Set by view-extensions/example-ext.js -->
<li>doSomething: yay fun roasted 1</li><!-- Custom filter defined by view-extensions/example-ext.js -->
</ul>
</body>
</html>
You can create sub-directories and organize your views however you'd like. Utilize Nunjucks' extends
and include
operators as you wish. Remember, paths are relative to the configured by viewPath
.
Server class. Must be instantiated to be used.
OkanjoServer.extensions.jsonpResponseCodeFix
– Extension that replaces non 200-level responses with 200 so non-ok level responses can execute on the browserOkanjoServer.extensions.responseErrorReporter
– Extension that reports 500-level responses via app.report, useful for production monitoring
server.app
– (read-only) The OkanjoApp instance provided when constructedserver.config
– (read-only) The configuration provided when constructedserver.options
– (read-only) The options provided when constructedserver.hapi
– (read-only) The HAPI instance created when initialized.server.io
– (read-only) The socket.io instance created when initialized.
Creates a new server instance.
app
– The OkanjoApp instance to bind toconfig
– (optional, object) The OkanjoServer configuration, see config.jsoptions
– (optional, object) Server options objectoptions.extensions
– Array of functions to call when initializing. Useful for initializing async hapi plugins or custom configurations.
For example:
new OkanjoServer(app, config, {
extensions: [
// Use the built-in extensions
OkanjoServer.extensions.jsonpResponseCodeFix, // replaces non 200-level responses with 200 so non-ok level responses can execute on the browser
OkanjoServer.extensions.responseErrorReporter, // reports
// Register a hapi extension, for example, query string parsing (like the old days)
async function giveMeQueryStringsBack() {
await this.hapi.register({
plugin: require('hapi-qs'),
options: {}
});
},
// Register authentication strategies, etc
async function registerAuthenticationStrategies() {
// plugin to use HTTP basic auth username as an api key
await this.hapi.register({
plugin: require('hapi-auth-basic-key'),
options: {}
});
// Register the strategy
this.hapi.auth.strategy('key-only', 'basic', {
validateFunc: (req, key, secret, authCallback) => {
// FIXME - put your real key authentication here (e.g. db or redis lookup)
let valid = key === 'my-secret-key';
let err = null;
// Pass back validity and credentials if valid
authCallback(err, valid, { key });
}
});
}
]
}, (err) => {
// server is configured, ready to start
});
Configures the underlying services, such as HAPI, Socket.io, etc. Called automatically by server.start
, if not done manually. Before v2, this was done in the constructor.
callback(err)
– Function to fire once the server has started. Iferr
is present, something went wrong.
Starts the server instance.
callback(err)
– Function to fire once the server has started. Iferr
is present, something went wrong.
Attempts to gracefully shutdown the server instance. If config.drainTime
elapses, the socket will be forcibly killed.
callback(err)
– Function to fire once the server has stopped. Iferr
is present, something went wrong.
This class fires no events.
Our goal is quality-driven development. Please ensure that 100% of the code is covered with testing.
Before contributing pull requests, please ensure that changes are covered with unit tests, and that all are passing.
To run unit tests and code coverage:
npm run report
This will perform:
- Unit tests
- Code coverage report
- Code linting
Sometimes, that's overkill to quickly test a quick change. To run just the unit tests:
npm test
or if you have mocha installed globally, you may run mocha test
instead.