okanjo-app-server
Version:
Server framework using HAPI and friends
370 lines (285 loc) • 13.7 kB
Markdown
# Okanjo App Server
[](https://travis-ci.org/Okanjo/okanjo-app-server) [](https://coveralls.io/github/Okanjo/okanjo-app-server?branch=master)
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](https://github.com/hapijs/hapi))
* Provides a consistent way for apps to define routes
* Serve static assets (via [inert](https://github.com/hapijs/inert))
* Render template views (via [vision](https://github.com/hapijs/vision) and [nunjucks](https://github.com/mozilla/nunjucks))
* Handle JSONP requests and error responses consistently
* Report bad request responses for dev/production debugging
* Run a WebSocket server (via [socket.io](https://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.
## Installing
Add to your project like so:
```sh
npm install okanjo-app-server
```
> Note: requires the [`okanjo-app`](https://github.com/okanjo/okanjo-app) module.
> Note: v2 and on uses Hapi v18+. Use v1 for Hapi 16
## Example Usage
Here's a super basic implementation.
Your directory structure might look like this:
* `example-app/`
* `routes/` – place to put your route files
* `example-routes.js` – example route file, seen below
* `static/` – place to put your static assets like css, images, js, etc
* `view-extensions/` – place to stick nunjucks extensions
* `example-ext.js` – example extension file, seen below
* `views/` – place to put your view templates
* `example.j2` – example template, seen below
* `config.js` – okanjo-app config
* `index.js` – app entrypoint
You can find these example files here: [docs/example-app](https://github.com/okanjo/okanjo-app-server/tree/master/docs/example-app)
#### config.js:
```js
"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.
#### index.js:
```js
"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.
#### routes/example-routes.js
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.
```js
"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' };
}
};
};
```
#### view-extensions/example-ext.js
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.
```js
"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/example.j2
Views are standard Nunjucks templates. For example:
```html
<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
<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`.
# OkanjoServer
Server class. Must be instantiated to be used.
## Statics
* `OkanjoServer.extensions.jsonpResponseCodeFix` – Extension that replaces non 200-level responses with 200 so non-ok level responses can execute on the browser
* `OkanjoServer.extensions.responseErrorReporter` – Extension that reports 500-level responses via app.report, useful for production monitoring
## Properties
* `server.app` – (read-only) The OkanjoApp instance provided when constructed
* `server.config` – (read-only) The configuration provided when constructed
* `server.options` – (read-only) The options provided when constructed
* `server.hapi` – (read-only) The HAPI instance created when initialized.
* `server.io` – (read-only) The socket.io instance created when initialized.
## Methods
### `new OkanjoServer(app, [config, [options]], [callback])`
Creates a new server instance.
* `app` – The OkanjoApp instance to bind to
* `config` – (optional, object) The OkanjoServer configuration, see [config.js](#config.js)
* `options` – (optional, object) Server options object
* `options.extensions` – Array of functions to call when initializing. Useful for initializing async hapi plugins or custom configurations.
For example:
```js
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
});
```
### `await server.init()`
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. If `err` is present, something went wrong.
### `await server.start()`
Starts the server instance.
* `callback(err)` – Function to fire once the server has started. If `err` is present, something went wrong.
### `await server.stop()`
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. If `err` is present, something went wrong.
## Events
This class fires no events.
## Extending and Contributing
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.
### Testing
To run unit tests and code coverage:
```sh
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:
```sh
npm test
```
or if you have mocha installed globally, you may run `mocha test` instead.