Create a Hapi.js server that serves static files
Use our shop frontend or roll out your own
const hapi = require('hapi');
const server = new hapi.Server();
server.connection({ port: 3000, host: 'localhost' });
server.route({
method: 'GET',
path: '/',
handler: function (request, reply) {
return reply('Hapi to see you!\n');
}
});
server.start(() => {
console.log(`Server running at ${server.info.uri}`);
});
Import the hapi
framework
Create the main applicaiton object - the server
By default the server is not listening on any port.
This is where connection
s come in.
There can be more than one connection to the server (eg. listen on multiple ports)
Register a route with the server. Routes need to be registered after the connections are setup.
Route configuration object contains the method
field
that can be set to any HTTP method or *
that matches all of them.
path
field may contain exact paths (/home
),
required params (/products/{id}
) and optional params (/files/{name?}
)
The handler recieves the request
object that contains all the information relevant to current request (params, headers, etc),
and the reply
interface which allows for sending responses.
After the config is done, start the server.
// npm install inert --save
const path = require('path');
const hapi = require('hapi');
const inert = require('inert');
const server = new hapi.Server();
server.connection({ port: 3000, host: 'localhost' });
server.register(inert, (err) => {
server.route({
method: 'GET',
path: '/{param*}',
handler: {
directory: {
path: path.join(__dirname, 'public'),
listing: true
}
}
});
server.start((err) => {
console.log(`Server running at: ${server.info.uri}`);
});
});
Install the inert
plugin
Plugins must be registered with the application server.
Because the registration is asynchronous rest of the config needs to be done in callback.
Thanks to inert
, hapi will recognize a directory
handler on this route
// npm install vision ejs --save
/** INDEX.JS **/
const hapi = require('hapi');
const vision = require('vision');
const ejs = require('ejs');
const server = new hapi.Server();
server.connection({ port: 3000, host: 'localhost' });
server.register(vision, () => {
server.views({
engines: { ejs },
relativeTo: __dirname,
path: 'views'
});
server.route({
method: 'GET',
path: '/',
handler: function (request, reply) {
return reply.view('index', { message: 'Hapi to see you!' });
}
});
server.start(() => {
console.log(`Server running at: ${server.info.uri}`);
});
});
/** VIEWS/INDEX.EJS **/
<h1><%= message %></h1>
Install the vision
plugin and ejs
templates package
Register vision
with the server
vision
extends the server object with views
method used to provide configuration for views.
Provide the templating engine. vision
supports multiple out-of-the-box and allows custom handlers.
On top of that, the reply
interface was exetened with the view
method.
It takes the template name and an object with data for the template as arguments.
The views/index.ejs
contains the tempalte.
All fields from the object provided to the template are available.
Create a Hapi.js server that serves static files
Use our shop frontend or roll out your own
Add GET /api/products
endpoint that returns a list of products
Keep the products in a separate JSON file
Generate the index.html
file using vision
(and templating engine of your choice)
Add GET /api/cart
endpoint that returns a list of products in the cart
Create an in-memory Products
database. It should be able to list, add, and remove products
// npm install --save blipp
const blipp = require('blipp');
server.route([
{
method: 'GET',
path: '/',
config: {
description: 'The homepage',
/* ... */
}
},
{ /* ... */ }
]);
server.register(blipp, () => {
server.start(/*...*/)
});
Install and import the blipp
plugin
Configure application routes as normal
Route's config contains the description
property: it will be usefull with logging and documentation generation.
Register the plugin and run the server
// npm install --save good good-console good-squeeze
const good = require('good');
server.route([
{
path: '/',
method: 'GET',
handler(request, reply) {
return reply({ message: 'Hapi to see you!' });
}
},
{
path: '/error',
method: 'GET',
handler() {
throw new Error();
}
}
]);
const goodOptions = {
reporters: {
consoleLogger: [
{
module: 'good-squeeze',
name: 'Squeeze',
args: [{
log: '*',
response: '*',
ops: '*',
error: '*'
}]
},
{
module: 'good-console'
},
'stdout'
]
}
};
server.register({ register: good, options: goodOptions }, () => {
server.start(() => { /* .. */ });
});
Install the good
plugin and good-console
reporter.
Register routes
Configuration object for the good
plugin.
Configure a new logger object. The configuration is an array of streams to pipie the logs through.
good-squeeze
is a utility that helps to filter stream events.
This reporter will log out all log
, response
, ops
and error
events fired by the server.
Next, format the logs with good-console
.
Finally, pass the logs to the stdout
stream (the actual console).
Register the plugin passing in the configuration object.
// npm install lab --save-dev
/* TEST/INDEX.JS */
const { expect, test, experiment } =
exports.lab =
require('lab').script();
experiment('Hapi to', () => {
test('be sane', (done) => {
expect(true).to.be.true();
done();
});
});
Install the lab
testing utility.
It's based on mocha
and shares most of the API.
Import the package and register a testing script with script
method.
The script has to be exported as lab
.
Get the tools for testing.
Mapping to mocha/jasmine
: experiment
is the same as describe
and test
corresponds to it
.
Change in relation to mocha
:
assertions are functions (true()
instead of true
). Easy to miss!
All lab
tests are asynchronous by default. Either call done()
or return a promise from the test.
/* LIB/SERVER.JS */
server.route({
method: 'GET',
path: '/',
handler: function (request, reply) {
return reply('Hapi to see you!\n');
}
});
module.exports = server;
/* TESTS/LIB/SERVER.JS */
const server = require('../lib/server');
const { expect, test, experiment } = exports.lab = require('lab').script();
experiment('Route: /', () => {
test('responds with 200', (done) => {
server.inject({ method: 'GET', path: '/' }, (res) => {
expect(res.statusCode).to.equal(200);
done();
});
});
});
Basic server definition wtih a route.
The server is exported to use it elswhere.
The server
object is decorated with inject
method thanks to the shot
utility.
The method enables faking server requests.
.inject
takes an object with options and a callback that will be called with a response
object.
Log all requests with good
and print routing table with blipp
Add POST /api/products
route. Validate the payload and use boom
for errors
Test GET /
and POST /api/products
endpoints
Add DELETE /api/products/{id}
route
Add POST /api/cart
route
Get 100% test coverage (lab -c
)
// npm install joi --save
const passwordSchema = joi.string()
.min(8)
.max(30);
joi.validate('password123', passwordSchema);
// { error: null, value: 'password123' }
joi.validate('hello', passwordSchema);
// { error: {...}, value: 'hello' }
Install and import the joi
library
Create a validation schema
The password must be a string
...
...between 8 and 30 characters long.
The validate
method takes the value to validate and the schema to validate against.
It returns and object/promise-like that contains error
and value
properties.
If there were any errors, their details will be shown in the error
property.
const idSchema = joi.string().token();
const productSchema = joi.object().keys({
id: idSchema,
price: joi.number().min(1)
});
server.route({
method: 'PUT',
path: '/products/{id}',
config: {
validate: {
headers: true,
params: {
id: idSchema.required()
},
query: false,
payload: productSchema
},
handler: function (request, reply) {
// ...
},
response: {
schema: productSchema,
failAction: 'error',
sample: 100
}
}
});
Define required schemas
Provide validation config
true
means that any value is allowed. In this case: all headers are OK.
Specifies validation per route param
false
- no values allowed. This request must not have any query params.
Payload of this request must match the productSchema
.
API responses can be validated too.
Validate the response against the productSchema
If the validation fails return the 500 Internal Server Error
response.
Percent of requests to validate. 100% is perfect for development.
Add joi
validation for the POST /api/prodcuts/
route
Add DELETE /api/products/{id}
route and validate the param
Reuse the product
validation schema to validate the response to GET /api/products
Add joi
validation to all routes
Get 100% test coverage (lab -c
)
Use either loki
or lowdb
and replace the in-memory database
/* /MANIFEST.JSS */
module.exports = {
"connections": [
{
"port": 3000,
"host": "localhost"
}
],
"registrations": [
{
"plugin": "inert"
},
{
"plugin": "vision"
}
]
};
/* /LIB/SERVER.JS */
const glue = require('glue');
const manifest = require('../manifest');
module.exports = glue.compose(manifest);
/* /INDEX.JS */
const Server = require('./lib/server');
Server.then((server) => {
server.route(/* ... */);
server.start().then(() => {
console.log(`Server running at ${server.info.uri}`);
});
});
manifest.js
or (manifest.json
) file in the main directory
Manifest contains the server connection configuration options
that would normally be passed to server.connection([options])
.
It also contains all the plugins along with their options.
Instead of creating the server manually,
use the glue
library to generate the server based on the manifest file.
Import the exported Server
definition.
glue.compose
(as well as most hapi
methods) returns a promise.
Once the server is ready, register routes as per usuall and start the server.
/* /LIB/PRODUCTS.jS */
exports.register = (server, options, next) => {
server.route({
method: 'GET',
path: '/',
description: 'Index route',
/* ... */
});
server.route({
method: 'GET',
path: '/{id}',
description: 'Fetch route',
/* ... */
});
/* ... */
next();
};
exports.register.attributes = {
name: 'Product routes'
};
/* /MANIFEST.JS */
module.exports = {
"connections": [/* ... */],
"registrations": [
/* ... */,
{
"plugin": "./lib/products",
"options": {
"routes": {
"prefix": "/products"
}
}
}
]
};
/* /LIB/SERVER.JS */
const glue = require('glue');
const manifest = require('../manifest');
const options = { relativeTo: __dirname };
module.exports = glue.compose(manifest, options);
Common way to split a Hapi app into smaller pieces is providing those pieces as plugins.
A plugin is essentialy just an object with the register
method that extends the server
object when called.
The consumer of the plugin can also provide options
that contain additional configuration.
When the register
method is called define routing for the products
resource.
After the setup is done, call next()
to return control back to the framework.
In this case the registraton is synchronous, but it's possible to register plugins asynchronously.
The register
method should have an attributes
property that contains plugin metadata.
Add the plugin to the server configuration manifest.
The plugin
property contains the path to the plugin module.
While registering the plugin that contains routes it's useful to provide the prefix.
Because the plugin is registered in a project directory it's required to add the relativeTo
option.
Split the application into plugins
Move the /api/products
prefix to plugin configuration
Write separate tests for a single plugin
Create separate package.json
files for each plugin
// npm install --save lout
const lout = require('lout');
server.register({ register: lout }, () => {
server.start(() => { /* .. */ });
});
// OR npm install --save hapi-swagger
const hapiSwagger = require('lout');
server.register({ register: hapiSwagger }, () => {
server.start(() => { /* .. */ });
});
// ROUTE CONFIG
server.route({
method: 'GET',
path: '/products/{id}/',
config: {
handler() { /* */},
description: 'Get a product',
notes: 'Returns a product by the id passed in the path',
tags: ['api'],
validate: {
params: {
id : Joi.string()
.token()
.required()
.description('The id for the product. Can only contain a-z, A-Z, 0-9, and underscore')
}
}
}
})
Standard installation and registration.
Documentation plugins heavily leverage route and validation properties like description
, notes
and tags
.
Open /docs
(with lout
) or /documentation
(hapi-swagger
)
/* /LIB/AUTHENTICATION.JS */
exports.register = (server, options, next) => {
server.auth.strategy(
'simple-authentication',
'basic',
false,
{
validateFunc(request, username, password, cb) {
/* ... */
cb(null, isAuthenticated, userCredentials)
}
}
);
next();
};
export.register.attributes = {
name: 'auth',
dependencies: 'hapi-auth-basic'
};
/* MANIFEST */
"registrations": [
{
"plugin": "hapi-auth-basic"
},
{
"plugin": "./lib/authentication"
},
/* ... */
]
/* ROUTE CONFIG */
server.route({
method: 'GET',
path: '/',
auth: 'simple-authentication',
handler(request, response) {
/* ... */
}
});
Authentication in HapiJS is build around the concepts of authentication scheme
and strategy
.
A scheme
is a Hapi plugin that defines the internals of authentication (ie. basic, digest, oauth, etc).
A strategy
is an instance of the scheme configured to work with our app.
There could be multiple strategies (configurations) for the same schema.
The strategy for the app is defined as a plugin.
Strategies are registered using the auth.strategy
method that takes up to 4 arguments.
First is the strategy's name: a server-wide unique identifier for this strategy.
Next, the schema
name. Also unique, provided by the schema plugin.
Authentication mode
defines the default behaviour:
false
means routes are not authenticated using this strategy by default.
Other possible values: true
, 'required'
, 'optional'
, 'try'
.
The configuration object.
Details depend on the schema
,
usually requires custom validation function.
If the plugin depends on some other plugins,
it's possible to define that in the attributes
object.
Register the auth
plugin along with it's dependency.
The auth
property allows to specify which strategy to use for this route.
Generate docs using lout
or hapi-swagger
.
Each route should have validation, tags, and descriptions.
Add authentication with hapi-auth-jwt2
Add blankie
and define strict Content Security Policy
Add a server method and matching route. Use caching.