HapiJS: A delightful experience

HapiJS: A delightful experience

This talk was given at the MVD-JS (Montevideo JavaScript meetup) by Matias Olivera (@moliveraf) and me (@espinosacurbelo).

Thanks to this amazing people:
- Eran Hammer (@eranhammer) - Hapi creator (http://hueniverse.com/2012/12/20/hapi-a-prologue/ & http://hueniverse.com/2014/08/20/performance-at-rest/)
- Dave Stevens (@shakefon) - Organise the project into plugins (https://medium.com/nerds-zappos/manifests-plugins-and-schemas-organizing-your-hapi-application-68cf316730ef)

71f946b4fdeb8a14e43297b2708e4c3a?s=128

Rodrigo Espinosa Curbelo

February 04, 2015
Tweet

Transcript

  1. A delightful experience

  2. Rodrigo Espinosa @espinosacurbelo Matías Olivera @moliveraf

  3. So… what is hapi?

  4. –Eran Hammer (hapi creator) “hapi was created around the idea

    that configuration is better than code, that business logic must be isolated from the transport layer...”
  5. Creating a server

  6. Create a new server object 1 var Hapi = require('hapi');

    2 3 var server = new Hapi.Server();
  7. Then, define a connection 1 var Hapi = require('hapi'); 2

    3 var server = new Hapi.Server(); 4 server.connection({ port: 3000 });
  8. Specifying the port in the connection 1 var Hapi =

    require('hapi'); 2 3 var server = new Hapi.Server(); 4 server.connection({ port: 3000 });
  9. Starting the server 1 var Hapi = require('hapi'); 2 3

    var server = new Hapi.Server(); 4 server.connection({ port: 3000 }); 5 6 server.start(function () { 7 8 console.log('Server running at:', server.info.uri); 9 });
  10. Adding routes

  11. Adding routes 1 var Hapi = require('hapi'); 2 3 var

    server = new Hapi.Server(); 4 server.connection({ port: 3000 }); 5 6 server.route({ 7 method: 'GET', 8 path: '/', 9 handler: function (request, reply) { 10 reply('Hello, world!'); 11 } 12 }); 13 14 server.start(function () { 15 16 console.log('Server running at:', server.info.uri); 17 });
  12. server.route takes a single configuration object { method: 'GET', path:

    '/', handler: function (request, reply) { reply('Hello, world!'); } }
  13. The HTTP verb you're responding to doesn't dictate the API

    method to use unlike express app.get(), app.post(), etc.
  14. Response with the reply function 1 var handler = function

    (request, reply) { 2 3 return reply('success'); 4 };
  15. Validating with Joi

  16. Input validation 1 server.route({ 2 method: 'GET', 3 path: '/hello/{name}',

    4 handler: function (request, reply) { 5 reply('Hello ' + request.params.name + '!'); 6 }, 7 config: { 8 validate: { 9 params: { 10 name: Joi.string().min(3).max(10) 11 } 12 } 13 } 14 });
  17. Input validation response for /hello/a { "error": "Bad Request", "message":

    "the length of name must be at least 3 characters long", "statusCode": 400, "validation": { "keys": [ "name" ], "source": "params" } }
  18. Query validation 1 server.route({ 2 method: 'GET', 3 path: '/list',

    4 handler: function (request, reply) { 5 reply(resources.slice(0, request.query.limit); 6 }, 7 config: { 8 validate: { 9 query: { 10 limit: Joi.number().integer().max(100) 11 } 12 } 13 } 14 });
  19. Query validation response for /list?limit=15&offset=15 { "error": "Bad Request", "message":

    "the key offset is not allowed", "statusCode": 400, "validation": { "keys": [ "offset" ], "source": "query" } }
  20. If you validate one key, by default you must validate

    them all
  21. It is also possible to validate incoming headers, with a

    validate.headers parameter
  22. You may also validate the payload data sent to a

    route, via the validate.payload parameter
  23. As in query validation, if you validate one key, you

    must validate them all
  24. Plugin system

  25. Simple plugin 1 var myPlugin = { 2 register: function

    (server, options, next) { 3 next(); 4 } 5 } 6 7 myPlugin.register.attributes = { 8 name: 'myPlugin', 9 version: '1.0.0' 10 };
  26. As a external module 1 exports.register = function (server, options,

    next) { 2 next(); 3 }; 4 5 exports.register.attributes = { 6 pkg: require('./package.json') 7 };
  27. Loading a plugin 1 server.register({register: require(‘myplugin')}, 2 function (err) {

    3 if (err) { 4 console.error('Failed to load plugin:’, 5 err); 6 } 7 });
  28. Loading multiple plugins 1 server.register([ 2 { 3 register: require('myplugin'),

    4 options: {} // options for 'myplugin' 5 },{ 6 register: require('yourplugin'), 7 options: {} // options for 'yourplugin' 8 } 9 ], function (err) { 10 if (err) { 11 console.error('Failed to load a plugin:', err); 12 } 13 });
  29. How powerful it is?

  30. Production ready

  31. protection against your process going out of memory

  32. event queue delay protection

  33. client request timeouts

  34. server response timeouts

  35. protection against aborted requests

  36. built-in request lifecycle logging

  37. input validation

  38. security headers

  39. People who faced it

  40. Walmart in Black Friday sales

  41. Walmart handle all mobile Black Friday traffic using hapi

  42. ~70% of their Black Friday traffic on mobile

  43. with about 10 CPU cores and 28Gb RAM

  44. Mind blowing traffic going through insignificant computing power (*)

  45. (*) “insignificant” by Walmart

  46. Composing a server

  47. Creating a server 1 var Hapi = require('hapi'); 2 3

    var server = new Hapi.Server(); 4 server.connection({ port: 3000 }); 5 6 server.start(function () { 7 8 console.log('Server running at:', server.info.uri); 9 });
  48. Composing a server 1 var Glue = require('glue'); 2 var

    manifest = { 3 connections: [{ 4 port: 80 5 }] 6 }; 7 8 Glue.compose(manifest, function (err, server) { 9 10 server.start(function (err) { 11 12 console.log('Server running at:’, 13 server.info.uri); 14 }); 15 });
  49. manifest.json { "server": { "app": { "slogan": "We push the

    web forward" } }, "connections": [ { "port": 80, "labels": ["web-ui"] }, { "port": 9000, "labels": ["api"] } ] }
  50. Composing a server 1 var Glue = require('glue'); 2 3

    Glue.compose(require(‘./config/manifest.json'), 4 function (err, server) { 5 6 server.start(function (err) { 7 8 // UI running on port 80, 9 // API running on port 9000 10 }); 11 });
  51. Organising a hapi project

  52. Organising into Controllers / |_ index.js |_ lib |_ controllers

    |_ about.js |_ home.js |_ search.js |_ views |_ about.html |_ home.html |_ layout.html |_ search.html
  53. lib/controllers/home.js 1 module.exports = function (request, reply) { 2 3

    var context = { 4 pageTitle: 'Home Page' 5 }; 6 7 reply.view('home', context); 8 };
  54. index.js 1 server.route({ 2 path: '/', 3 method: 'GET', 4

    handler: require('./lib/controllers/home') 5 });
  55. Simple and manageable pattern, but…

  56. Why don’t we modularize the different parts of functionality?

  57. Organising into Plugins / |_ index.js |_ lib |_ modules

    |_ company |_ about.js |_ home.js |_ index.js |_ package.json |_ search |_ index.js |_ package.json |_ search.js |_ templates |_ about.html |_ home.html |_ layout.html |_ search.html
  58. lib/modules/company/ index.js 1 exports.register = function (server, options, next) {

    2 3 server.route({ 4 path: '/', 5 method: 'GET', 6 handler: require('./home') 7 }); 8 9 server.route({ 10 path: '/about', 11 method: 'GET', 12 handler: require('./about') 13 }); 14 15 next(); 16 }; 17 18 exports.register.attributes = { 19 pkg: require('./package.json') 20 };
  59. lib/modules/company/package.json { "name": "exampleCompanySection", "version": "1.0.0" }

  60. lib/modules/company/ home.js 1 module.exports = function (request, reply) { 2

    3 var context = { 4 pageTitle: 'Home Page' 5 }; 6 7 reply.view('home', context); 8 };
  61. manifest.json { "server": { "app": { "slogan": "We push the

    web forward" } }, "connections": [ { "port": 80, "labels": ["web-ui"] }, { "port": 9000, "labels": ["api"] } ], "plugins": { "./company": null, "./search": null } }
  62. index.js 1 var Glue = require('glue'); 2 var manifest =

    require('./config/manifest.json'); 3 var options = { 4 relativeTo: process.cwd() + '/lib/modules' 5 }; 6 7 Glue.compose(manifest, options, function (err, server) { 8 9 server.start(function (err) { 10 11 // UI running on port 80, 12 // API running on port 9000 13 }); 14 });
  63. Thanks.