├── .gitignore ├── .jshintignore ├── .jshintrc ├── README.md ├── config.dev.js ├── config.js ├── config.prod.js ├── config.staging.js ├── config.test.js ├── lib ├── boot │ ├── jobs-loader.js │ ├── routes-loader.js │ └── services-loader.js ├── config │ ├── auth.js │ ├── jobs.js │ ├── routes.js │ └── services.js ├── index.js ├── jobs │ └── example-job.js ├── models │ ├── example-model.js │ └── index.js ├── routes │ ├── example-route.js │ └── validations │ │ └── example-route.js └── services │ └── example-service.js ├── package.json ├── server.js └── test └── example-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | heapdump* 2 | poop.log* 3 | logs/* 4 | *.log 5 | npm-debug.log* 6 | pids 7 | *.pid 8 | *.seed 9 | lib-cov 10 | coverage 11 | .lock-wscript 12 | build/Release 13 | node_modules 14 | .npm 15 | .node_repl_history 16 | *.sublime-project 17 | *.sublime-workspace 18 | *.DS_Store 19 | .idea 20 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 50, // {int} Maximum error before stopping 3 | 4 | // Enforcing 5 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 6 | "camelcase" : false, // true: Identifiers must be in camelCase 7 | "curly" : true, // true: Require {} for every new block or scope 8 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 9 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 10 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 11 | "immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 12 | // "latedef" : true, // true: Require variables/functions to be defined before being used 13 | "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` 14 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 15 | "noempty" : true, // true: Prohibit use of empty blocks 16 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 17 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 18 | "plusplus" : false, // true: Prohibit use of `++` and `--` 19 | "quotmark" : false, // Quotation mark consistency: 20 | // false : do nothing (default) 21 | // true : ensure whatever is used is consistent 22 | // "single" : require single quotes 23 | // "double" : require double quotes 24 | //"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 25 | //"unused" : true, // Unused variables: 26 | // true : all variables, last function parameter 27 | // "vars" : all variables only 28 | // "strict" : all variables, all function parameters 29 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 30 | "maxparams" : false, // {int} Max number of formal params allowed per function 31 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 32 | "maxstatements" : false, // {int} Max number statements per function 33 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 34 | "maxlen" : 80, // {int} Max number of characters per line 35 | "varstmt" : false, // true: Disallow any var statements. Only `let` and `const` are allowed. 36 | 37 | // Relaxing 38 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 39 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 40 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 41 | "eqnull" : false, // true: Tolerate use of `== null` 42 | "es5" : true, // true: Allow ES5 syntax (ex: getters and setters) 43 | "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) 44 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 45 | // (ex: `for each`, multiple try/catch, function expression…) 46 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 47 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 48 | "funcscope" : false, // true: Tolerate defining variables inside control statements 49 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 50 | "iterator" : false, // true: Tolerate using the `__iterator__` property 51 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 52 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 53 | "laxcomma" : false, // true: Tolerate comma-first style coding 54 | "loopfunc" : false, // true: Tolerate functions being defined in loops 55 | "multistr" : false, // true: Tolerate multi-line strings 56 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 57 | "notypeof" : false, // true: Tolerate invalid typeof operator values 58 | "proto" : false, // true: Tolerate using the `__proto__` property 59 | "scripturl" : false, // true: Tolerate script-targeted URLs 60 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 61 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 62 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 63 | "validthis" : false, // true: Tolerate using this in a non-constructor function 64 | 65 | // Environments 66 | "browser" : true, // Web Browser (window, document, etc) 67 | "browserify" : false, // Browserify (node.js code in the browser) 68 | "couch" : false, // CouchDB 69 | "devel" : true, // Development/debugging (alert, confirm, etc) 70 | "dojo" : false, // Dojo Toolkit 71 | "jasmine" : false, // Jasmine 72 | "jquery" : false, // jQuery 73 | "mocha" : true, // Mocha 74 | "mootools" : false, // MooTools 75 | "node" : true, // Node.js 76 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 77 | "phantom" : false, // PhantomJS 78 | "prototypejs" : false, // Prototype and Scriptaculous 79 | "qunit" : false, // QUnit 80 | "rhino" : false, // Rhino 81 | "shelljs" : false, // ShellJS 82 | "typed" : false, // Globals for typed array constructions 83 | "worker" : false, // Web Workers 84 | "wsh" : false, // Windows Scripting Host 85 | "yui" : false, // Yahoo User Interface 86 | 87 | // Custom Globals 88 | "globals": { 89 | "_": true, 90 | "App": true, 91 | "$": true, 92 | "jQuery": true, 93 | "afterEach": true, 94 | "beforeEach": true, 95 | "describe": true, 96 | "expect": true, 97 | "getJSONFixture": true, 98 | "it": true, 99 | "jasmine": true, 100 | "process": true, 101 | "global": true, 102 | "Meteor": true, 103 | "module": true, 104 | "sinon": true 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hapi Chairo Api Boilerplate 2 | 3 | # DEPRECATION NOTICE 4 | This project is no longer mantained. 5 | Use [generator-hapi-api-stack](https://github.com/franzip/generator-hapi-api-stack) instead. 6 | 7 | Table of Contents 8 | ================= 9 | 10 | * [Hapi Chairo Api Boilerplate](#hapi-chairo-api-boilerplate) 11 | * [Table of Contents](#table-of-contents) 12 | * [Usage](#usage) 13 | * [Plugins loading and registration](#plugins-loading-and-registration) 14 | * [Jobs, Routes, Auth, Models and Services loading](#jobs-routes-auth-models-and-services-loading) 15 | * [Auth Strategies](#auth-strategies) 16 | * [Routes](#routes) 17 | * [Prefix](#prefix) 18 | * [Models](#models) 19 | * [Jobs](#jobs) 20 | * [Services](#services) 21 | * [Project Structure](#project-structure) 22 | * [Packages Docs](#packages-docs) 23 | * [Hapi](#hapi) 24 | * [Models](#models-1) 25 | * [Services](#services-1) 26 | * [Jobs](#jobs-1) 27 | * [Tests](#tests) 28 | * [Utils](#utils) 29 | 30 | ## Usage 31 | 32 | Run ```npm install``` as usual 33 | 34 | ```npm run dev``` will run the server with ```config.dev.js``` settings 35 | 36 | ```npm run staging``` will run the server with ```config.staging.js``` settings 37 | 38 | ```npm run prod``` will run the server with ```config.prod.js``` settings 39 | 40 | ## Plugins loading and registration 41 | You can easily customize your Hapi server by adding the plugins you need in ```/server.js``` [manifest file](https://github.com/hapijs/glue/blob/master/API.md#usage) or in ```/lib/index.js```. 42 | 43 | ## Jobs, Routes, Auth, Models and Services loading 44 | Everything has been setup in order to avoid the need to write code to add new components to the server. 45 | Anyway, since this is a boilerplate and the loading process cannot be completely abstracted, you'll probably need to tweak various things around if you plan to use different packages for models, jobs, services, etc... 46 | 47 | ### Auth Strategies 48 | You can add your authentication strategies in ```/lib/config/auth.js``` and they will be registered automatically. 49 | The default authentication strategy is set in ```/lib/index.js```. 50 | 51 | ### Routes 52 | Adding a new route is a two step operation: 53 | 1. Create a route as ```/lib/routes/routename.js``` 54 | 2. Add ```routename``` into ```/lib/config/routes.js``` 55 | 56 | #### Prefix 57 | 58 | Every registered route is prefixed by a prefix and a version (```/api/v1``` is the default). 59 | ```API_PREFIX```and ```API_VERSION``` are defined in ```/lib/config/routes.js```. 60 | 61 | ### Models 62 | To add a new model, just create a new filename into ```/lib/models/``` and it will be loaded automatically. 63 | 64 | ### Jobs 65 | Adding a new job is a two step operation: 66 | 1. Create a job into ```/lib/jobs/jobname.js``` 67 | 2. Add ```jobname``` into ```/lib/config/jobs.js``` 68 | 69 | ### Services 70 | Adding a new service is a two step operation: 71 | 1. Create a service into ```/lib/jobs/servicename.js``` 72 | 2. Add ```servicename``` into ```/lib/config/services.js``` 73 | 74 | ## Project Structure 75 | 76 | ```bash 77 | hapi-chairo-api-boilerplate 78 | ├── lib 79 | │ ├── boot # loader utils 80 | │ ├── config # configs 81 | │ ├── jobs # jobs 82 | │ ├── models # models 83 | │ ├── services # services 84 | │ ├── routes # routes 85 | │ └── index.js # plugins loading and registrations 86 | │ 87 | ├── test 88 | │ ├── example-test.js 89 | │ 90 | ├── logs 91 | │ 92 | ├── server.js # server composition 93 | ├── config.js # setup ENV config 94 | ├── config.dev.js 95 | ├── config.staging.js 96 | ├── config.prod.js 97 | ├── package.json 98 | └── README.md 99 | ``` 100 | 101 | ## Packages Docs 102 | 103 | ### Hapi 104 | - [Hapi](https://github.com/hapijs/hapi/blob/master/API.md) | Server Framework 105 | - [Glue](https://github.com/hapijs/glue/blob/master/API.md) | Server Composer 106 | - [Poop](https://github.com/hapijs/poop/blob/master/README.md) | Logs uncaught exceptions 107 | - [Boom](https://github.com/hapijs/boom/blob/master/README.md) | HTTP errors 108 | - [Joi](https://github.com/hapijs/joi/blob/v7.2.3/API.md) | Object Schema validation 109 | 110 | ### Models 111 | - [Hapi Mongo Models](https://github.com/jedireza/hapi-mongo-models/blob/master/README.md) | Mongo Models for Hapi 112 | 113 | ### Services 114 | - [Seneca](https://github.com/senecajs/seneca/blob/master/README.md) | Microservices Toolkit 115 | - [Chairo](https://github.com/hapijs/chairo/blob/master/README.md) | SenecaJS/Hapi integration 116 | 117 | ### Jobs 118 | - [Agenda](https://github.com/rschmukler/agenda/blob/master/README.md) | Job scheduler 119 | 120 | ### Tests 121 | - [Lab](https://github.com/hapijs/lab/blob/master/README.md) 122 | - [Code](https://github.com/hapijs/code/blob/master/API.md) 123 | 124 | ### Utils 125 | - [Underscore](http://underscorejs.org/) 126 | - [Chalk](https://github.com/chalk/chalk/blob/master/readme.md) 127 | - [Hoek](https://github.com/hapijs/hoek/blob/master/README.md) 128 | 129 | ## TODO 130 | 131 | - [ ] Better logs handling 132 | - [ ] API Docs support 133 | -------------------------------------------------------------------------------- /config.dev.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Put Server and Plugins configs here 3 | * ENV: Development 4 | ******************************************************************************/ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | 10 | module.exports = { 11 | 12 | env: 'development', 13 | 14 | product: { 15 | name: 'Your Awesome API Server' 16 | }, 17 | 18 | server: { 19 | yourServerName: { 20 | host: '0.0.0.0', 21 | port: process.env.PORT || 3000, 22 | tls: false 23 | } 24 | }, 25 | 26 | poop: { 27 | logPath: path.join(__dirname, 'poop.log'), 28 | heapdumpFolder: path.join(__dirname, '/logs'), 29 | }, 30 | 31 | jobs: { 32 | MONGO_ADDRESS: "localhost:27017/yourdbname", 33 | COLLECTION_NAME: "jobs", 34 | }, 35 | 36 | chairo: { 37 | }, 38 | 39 | mongoModels: { 40 | mongodb: { 41 | url: 'mongodb://localhost:27017/yourdbname', 42 | options: {}, 43 | }, 44 | autoIndex: false, 45 | models: require('./lib/models') 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const env = require('get-env')({ 4 | staging: 'staging', 5 | test: 'test' 6 | }); 7 | 8 | module.exports = require('./config.' + env); 9 | -------------------------------------------------------------------------------- /config.prod.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Put Server and Plugins configs here 3 | * ENV: Production 4 | ******************************************************************************/ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | 10 | module.exports = { 11 | 12 | env: 'development', 13 | 14 | product: { 15 | name: 'Your Awesome API Server' 16 | }, 17 | 18 | server: { 19 | yourServerName: { 20 | host: '0.0.0.0', 21 | port: process.env.PORT || 3000, 22 | tls: true 23 | } 24 | }, 25 | 26 | poop: { 27 | logPath: path.join(__dirname, 'poop.log'), 28 | heapdumpFolder: path.join(__dirname, '/logs'), 29 | writeStreamOptions: { 30 | flags: 'a' 31 | } 32 | }, 33 | 34 | jobs: { 35 | MONGO_ADDRESS: "localhost:27017/yourdbname", 36 | COLLECTION_NAME: "jobs", 37 | }, 38 | 39 | chairo: { 40 | }, 41 | 42 | mongoModels: { 43 | mongodb: { 44 | url: 'mongodb://localhost:27017/yourdbname', 45 | options: {}, 46 | }, 47 | autoIndex: false, 48 | models: require('./lib/models') 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /config.staging.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Put Server and Plugins configs here 3 | * ENV: Staging 4 | ******************************************************************************/ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | 10 | module.exports = { 11 | 12 | env: 'development', 13 | 14 | product: { 15 | name: 'Your Awesome API Server' 16 | }, 17 | 18 | server: { 19 | yourServerName: { 20 | host: '0.0.0.0', 21 | port: process.env.PORT || 3000, 22 | tls: false 23 | } 24 | }, 25 | 26 | poop: { 27 | logPath: path.join(__dirname, 'poop.log'), 28 | heapdumpFolder: path.join(__dirname, '/logs'), 29 | }, 30 | 31 | jobs: { 32 | MONGO_ADDRESS: "localhost:27017/yourdbname", 33 | COLLECTION_NAME: "jobs", 34 | }, 35 | 36 | chairo: { 37 | }, 38 | 39 | mongoModels: { 40 | mongodb: { 41 | url: 'mongodb://localhost:27017/yourdbname', 42 | options: {}, 43 | }, 44 | autoIndex: false, 45 | models: require('./lib/models') 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /config.test.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Put Server and Plugins configs here 3 | * ENV: Test 4 | ******************************************************************************/ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | 10 | module.exports = { 11 | 12 | env: 'test', 13 | 14 | product: { 15 | name: 'Your Awesome API Server' 16 | }, 17 | 18 | server: { 19 | yourServerName: { 20 | host: '0.0.0.0', 21 | port: process.env.PORT || 3000, 22 | tls: false 23 | } 24 | }, 25 | 26 | poop: { 27 | logPath: path.join(__dirname, 'poop.log'), 28 | heapdumpFolder: path.join(__dirname, '/logs'), 29 | }, 30 | 31 | jobs: { 32 | MONGO_ADDRESS: "localhost:27017/yourdbname", 33 | COLLECTION_NAME: "jobs", 34 | }, 35 | 36 | chairo: { 37 | }, 38 | 39 | mongoModels: { 40 | mongodb: { 41 | url: 'mongodb://localhost:27017/yourdbname', 42 | options: {}, 43 | }, 44 | autoIndex: false, 45 | models: require('./lib/models') 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /lib/boot/jobs-loader.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Jobs Loader 3 | * To load new jobs, add the job filename into /lib/config/jobs.js 4 | ******************************************************************************/ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | const chalk = require('chalk'); 10 | const _ = require('underscore'); 11 | const config = require('../config/jobs'); 12 | 13 | module.exports = function jobsLoader(server) { 14 | 15 | const loader = _.compose(_.bind(loadJobs, null, server), 16 | getJobs, 17 | getJobsPaths); 18 | 19 | loader(config.jobsNames)(); 20 | }; 21 | 22 | // load all jobs, set up agenda and decorate server object with agenda instance 23 | function loadJobs(server, jobs) { 24 | var agenda = new require('agenda')(); 25 | 26 | agenda 27 | .database(config.globals.MONGO_ADDRESS, config.globals.COLLECTION_NAME); 28 | 29 | return function loadAllJobs() { 30 | _.each(jobs, function loadJob(job) { 31 | job(server, agenda); 32 | }); 33 | 34 | agenda.on('error', function agendaError(err) { 35 | console.error(chalk.bgRed.white(err)); 36 | }); 37 | 38 | agenda.on('ready', function agendaReady() { 39 | console.log(chalk.bgGreen.white('Jobs queue starting up...')); 40 | agenda.start(); 41 | }); 42 | 43 | server.decorate('server', 'jobs', agenda); 44 | }; 45 | } 46 | 47 | // pack the actual jobs into an array 48 | function getJobs(jobPaths) { 49 | return _.flatten( 50 | _.map(jobPaths, function jobGetter(jobPath) { 51 | return require(jobPath); 52 | }) 53 | ); 54 | } 55 | 56 | // extract absolute routes paths from array of route names 57 | function getJobsPaths(jobNames) { 58 | return _.map(jobNames, function jobPathGetter(jobName) { 59 | return path.join( 60 | __dirname, '..', config.globals.JOBS_FOLDER, jobName 61 | ); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /lib/boot/routes-loader.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Routes Loader 3 | * To load new routes, add the route filename into /lib/config/routes.js 4 | ******************************************************************************/ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | const chalk = require('chalk'); 10 | const _ = require('underscore'); 11 | const config = require('../config/routes'); 12 | 13 | module.exports = function routesLoader(server, options) { 14 | 15 | const loader = _.compose(_.bind(registerRoutes, null, server, options), 16 | wrapRoutes, 17 | prefixRoutes, 18 | getRoutes, 19 | getRoutesPaths); 20 | 21 | loader(config.routesNames)(); 22 | }; 23 | 24 | // register all routes 25 | function registerRoutes(server, options, routes) { 26 | return function registerAllRoutes() { 27 | console.log(chalk.bgGreen.white('Registering Routes...')); 28 | _.each(routes, function registerRoute(route) { 29 | server.route(route(server, options)); 30 | }); 31 | }; 32 | } 33 | 34 | // wrap routes into functions so we can pass server and options around 35 | function wrapRoutes(routes) { 36 | return _.map(routes, function routeWrapper(route) { 37 | return function singleRouteWrapper(server, options) { 38 | return route; 39 | }; 40 | }); 41 | } 42 | 43 | // prefix routes 44 | function prefixRoutes(routes) { 45 | return _.map(routes, function routePrefixer(route) { 46 | route.path = config.globals.API_PREFIX + 47 | config.globals.API_VERSION + 48 | route.path; 49 | return route; 50 | }); 51 | } 52 | 53 | // pack the actual routes into an array 54 | function getRoutes(routePaths) { 55 | return _.flatten( 56 | _.map(routePaths, function requestRoute(routePath) { 57 | return require(routePath)(); 58 | }) 59 | ); 60 | } 61 | 62 | // extract absolute routes paths from array of route names 63 | function getRoutesPaths(routeNames) { 64 | return _.map(routeNames, function routePathGetter(routeName) { 65 | return path.join( 66 | __dirname, '..', config.globals.ROUTES_FOLDER, routeName 67 | ); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /lib/boot/services-loader.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Services Loader 3 | * To load new services, add the service filename into /lib/config/services.js 4 | ******************************************************************************/ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | const chalk = require('chalk'); 10 | const _ = require('underscore'); 11 | const config = require('../config/services'); 12 | 13 | module.exports = function servicesLoader(server, options) { 14 | 15 | const loader = _.compose(_.bind(loadServices, null, server, options), 16 | getServices, 17 | getServicesPaths); 18 | 19 | loader(config.servicesNames)(); 20 | }; 21 | 22 | // attach each service to the server object 23 | function loadServices(server, options, services) { 24 | return function loadAllServices() { 25 | console.log(chalk.bgGreen.white('Loading Services...')); 26 | _.each(services, function mountService(service) { 27 | service(server, options); 28 | }); 29 | }; 30 | } 31 | 32 | // pack the actual services into an array 33 | function getServices(servicesPaths) { 34 | return _.map(servicesPaths, function requestService(servicePath) { 35 | return require(servicePath); 36 | }); 37 | } 38 | 39 | // extract absolute services paths from array of route names 40 | function getServicesPaths(servicesNames) { 41 | return _.map(servicesNames, function servicePathGetter(serviceName) { 42 | return path.join( 43 | __dirname, '..', config.globals.SERVICES_FOLDER, serviceName 44 | ); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /lib/config/auth.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Auth config and related constants go here 3 | ******************************************************************************/ 4 | 5 | 'use strict'; 6 | 7 | module.exports = { 8 | // declare you strategies here 9 | exampleStrategy: function exampleStrategy(server) { 10 | server.auth.strategy('simple', 'bearer-access-token', { 11 | allowQueryToken: false, 12 | allowMultipleHeaders: true, 13 | accessTokenName: 'token', 14 | tokenType: 'token', 15 | // implement a real validation here 16 | validateFunc: function exampleStrategyValidationFn(token, callback) { 17 | return callback(null, true, {token: token}); 18 | } 19 | }); 20 | }, 21 | // anotherStrategy: function anotherStrategy(server) { 22 | // ... 23 | // } 24 | }; 25 | -------------------------------------------------------------------------------- /lib/config/jobs.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Jobs config and related constants go here 3 | ******************************************************************************/ 4 | 5 | 'use strict'; 6 | 7 | const config = require('../../config'); 8 | 9 | module.exports = { 10 | globals: { 11 | MONGO_ADDRESS: config.jobs.MONGO_ADDRESS, 12 | COLLECTION_NAME: config.jobs.COLLECTION_NAME, 13 | JOBS_FOLDER: 'jobs', 14 | FREQUENCY: 'one minute', 15 | MAX_CONCURRENCY: 5 16 | }, 17 | // add new jobs by name here (must match /jobs/{jobname}.js) 18 | jobsNames: [ 19 | 'example-job' 20 | ] 21 | }; 22 | -------------------------------------------------------------------------------- /lib/config/routes.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Routes config and related constants go here 3 | ******************************************************************************/ 4 | 5 | 'use strict'; 6 | 7 | module.exports = { 8 | globals: { 9 | API_PREFIX: '/api', 10 | API_VERSION: '/v1', 11 | ROUTES_FOLDER: 'routes', 12 | }, 13 | // add new routes by name here (must match /routes/{routename}.js) 14 | routesNames: [ 15 | 'example-route' 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /lib/config/services.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Services config and related constants go here 3 | ******************************************************************************/ 4 | 5 | 'use strict'; 6 | 7 | module.exports = { 8 | globals: { 9 | SERVICES_FOLDER: 'services', 10 | }, 11 | exampleService: { 12 | }, 13 | // add new services by name here (must match /services/{servicename}.js) 14 | servicesNames: [ 15 | 'example-service', 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Load more stuff on the Server here (Routes, Jobs, Services, etc.).. 3 | ******************************************************************************/ 4 | 5 | 'use strict'; 6 | 7 | const _ = require('underscore'); 8 | const config = require('../config.js'); 9 | const authStrategies = require('./config/auth'); 10 | const routesLoader = require('./boot/routes-loader'); 11 | const servicesLoader = require('./boot/services-loader'); 12 | const jobsLoader = require('./boot/jobs-loader'); 13 | const Package = require('../package.json'); 14 | 15 | exports.register = function(server, options, next) { 16 | 17 | server.register([ 18 | { 19 | register: require('chairo'), 20 | options: config.chairo 21 | }, 22 | { 23 | register: require('hapi-auth-bearer-token') 24 | } 25 | ], 26 | (err) => { 27 | 28 | if (err) { 29 | return next(err); 30 | } 31 | 32 | // load all auth strategies 33 | _.each(Object.keys(authStrategies), (authStrategy) => { 34 | authStrategies[authStrategy].call(null, server); 35 | }); 36 | // set server default strategy 37 | server.auth.default('simple'); 38 | // load all services 39 | servicesLoader(server, options); 40 | // load all routes 41 | routesLoader(server, options); 42 | // load all jobs 43 | jobsLoader(server); 44 | 45 | next(); 46 | }); 47 | }; 48 | 49 | exports.register.attributes = { 50 | pkg: Package 51 | }; 52 | -------------------------------------------------------------------------------- /lib/jobs/example-job.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | 5 | module.exports = function exampleJob(server, agenda) { 6 | 7 | agenda.define('exampleJob', function(job, done) { 8 | console.log('Executing job with data: '); 9 | console.log(job.attrs.data); 10 | return done(null, {}); 11 | }); 12 | 13 | agenda.on('success:exampleJob', (job) => { 14 | var msg = "Job with ID: " + job.attrs._id + " executed with success."; 15 | console.log(chalk.bgGreen.white(msg)); 16 | job.remove((err) => { 17 | if (!err) { 18 | console.log(chalk.bgGreen.white('Job removed successfully.')); 19 | } else { 20 | console.error(chalk.bgRed.white('An error occurred while removing the Job.')); 21 | } 22 | }); 23 | }); 24 | 25 | agenda.on('fail:exampleJob', (err, job) => { 26 | var msg = "An error occurred while processing Job with ID: " + job.attrs._id; 27 | console.error(chalk.bgRed.white(msg)); 28 | console.error(chalk.bgRed.white(err)); 29 | job.remove((err) => { 30 | if (!err) { 31 | console.log(chalk.bgGreen.white('Job removed successfully.')); 32 | } else { 33 | console.error(chalk.bgRed.white('An error occurred while removing the Job.')); 34 | } 35 | }); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/models/example-model.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Example Model 3 | ******************************************************************************/ 4 | 5 | 'use strict'; 6 | 7 | const joi = require('joi'), 8 | objectAssign = require('object-assign'), 9 | BaseModel = require('hapi-mongo-models').BaseModel; 10 | 11 | const Example = BaseModel.extend({ 12 | constructor: function(attrs) { 13 | objectAssign(this, attrs); 14 | } 15 | }); 16 | 17 | Example._collection = 'example-collection'; 18 | 19 | Example.schema = joi.object().keys({ 20 | name: joi.string().required() 21 | }); 22 | 23 | module.exports = Example; 24 | -------------------------------------------------------------------------------- /lib/models/index.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Models loader 3 | * Create new models into lib/models/ and they will be loaded automatically 4 | ******************************************************************************/ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | const _ = require('underscore'); 10 | const fs = require('fs'); 11 | 12 | module.exports = {}; 13 | 14 | _.each(fs.readdirSync(__dirname), (fileName) => { 15 | if (fileName === 'index.js') { 16 | return; 17 | } 18 | 19 | _.extend(module.exports, modelObjectFactory(fileName)); 20 | }); 21 | 22 | function modelObjectFactory(fileName) { 23 | var modelObj = Object.create(null), 24 | modelPath = path.join(__dirname, fileName), 25 | modelName = fileName.slice(0, fileName.indexOf('.')); 26 | 27 | modelName = modelName.charAt(0).toUpperCase() + modelName.slice(1); 28 | modelObj[modelName] = modelPath; 29 | 30 | return modelObj; 31 | } 32 | -------------------------------------------------------------------------------- /lib/routes/example-route.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Example Routes 3 | ******************************************************************************/ 4 | 5 | 'use strict'; 6 | 7 | const boom = require('boom'); 8 | const exampleValidation = require('./validations/example-route'); 9 | 10 | module.exports = (server, options) => { 11 | return [ 12 | { 13 | method: 'GET', 14 | path: '/example', 15 | config: { 16 | handler: (request, reply) => { 17 | return reply('GET example'); 18 | } 19 | } 20 | }, 21 | { 22 | method: 'POST', 23 | path: '/example', 24 | config: { 25 | handler: (request, reply) => { 26 | const exampleModel = request.server.plugins['hapi-mongo-models']['Example-model']; 27 | var response; 28 | 29 | exampleModel.insertOne({name: request.payload.name}, (err, result) => { 30 | response = (err) ? err : result; 31 | 32 | return reply(response); 33 | }); 34 | }, 35 | validate: require('./validations/example-route').createOne 36 | } 37 | }, 38 | { 39 | method: 'GET', 40 | path: '/example/{id}', 41 | config: { 42 | handler: (request, reply) => { 43 | return reply(request.params.id); 44 | }, 45 | validate: require('./validations/example-route').getOne 46 | } 47 | }, 48 | { 49 | method: 'POST', 50 | path: '/example/job', 51 | config: { 52 | handler: (request, reply) => { 53 | const serverJobs = request.server.jobs; 54 | 55 | var data = request.payload.data, 56 | exampleJob = serverJobs.create('exampleJob', data), 57 | response; 58 | 59 | exampleJob.save((err) => { 60 | if (err) { 61 | response = reply(boom.badImplementation()); 62 | } else { 63 | response = reply().created(''); 64 | } 65 | }); 66 | 67 | return response; 68 | }, 69 | validate: require('./validations/example-route').createJob 70 | } 71 | }, 72 | { 73 | method: 'POST', 74 | path: '/example/microservice', 75 | config: { 76 | handler: (request, reply) => { 77 | return reply.act({ generate: 'id' }); 78 | } 79 | } 80 | }, 81 | ]; 82 | }; 83 | -------------------------------------------------------------------------------- /lib/routes/validations/example-route.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Example Validations 3 | ******************************************************************************/ 4 | 5 | 'use strict'; 6 | 7 | const joi = require('joi'); 8 | 9 | module.exports = { 10 | getOne: { 11 | params: { 12 | id: joi.string().alphanum().required() 13 | } 14 | }, 15 | 16 | createOne: { 17 | payload: { 18 | name: joi.string().min(2).max(20).required() 19 | } 20 | }, 21 | 22 | createJob: { 23 | payload: { 24 | data: joi.object().required() 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /lib/services/example-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function exampleService(server, options) { 4 | var id = 0; 5 | server.seneca.add({ generate: 'id' }, function (message, next) { 6 | 7 | return next(null, { id: ++id }); 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-api-boilerplate", 3 | "version": "0.1.0", 4 | "description": "Hapi API Server Boilerplate", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "start": "mkdir -p logs; node --harmony server.js", 8 | "dev": "mkdir -p logs; NODE_ENV=dev node --harmony server.js", 9 | "staging": "mkdir -p logs; NODE_ENV=staging node --harmony server.js", 10 | "prod": "mkdir -p logs; NODE_ENV=prod node --harmony server.js", 11 | "test": "lab" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/franzip/hapi-api-boilerplate" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/franzip/hapi-api-boilerplate/issues" 19 | }, 20 | "engines": { 21 | "node": ">=4.0.0" 22 | }, 23 | "dependencies": { 24 | "agenda": "0.8.x", 25 | "boom": "3.x.x", 26 | "chairo": "2.x.x", 27 | "chalk": "1.x.x", 28 | "get-env": "0.4.x", 29 | "glue": "3.2.x", 30 | "hapi": "13.x.x", 31 | "hapi-mongo-models": "4.x.x", 32 | "hapi-auth-bearer-token": "4.x.x", 33 | "poop": "2.x.x", 34 | "underscore": "1.8.x" 35 | }, 36 | "devDependencies": { 37 | "code": "2.x.x", 38 | "eslint-config-google": "^0.4.0", 39 | "jshint": "2.x.x", 40 | "lab": "10.x.x" 41 | }, 42 | "keywords": [ 43 | "hapi", 44 | "api", 45 | "boilerplate", 46 | "microservice" 47 | ], 48 | "author": "Francesco Pezzella ", 49 | "license": "MIT" 50 | } 51 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * App Manifest and Server composition 3 | * Do not register Hapi plugins manually, do it here instead 4 | ******************************************************************************/ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'), 9 | chalk = require('chalk'), 10 | glue = require('glue'), 11 | config = require('./config'); 12 | 13 | config.server.yourServerName.uri = (config.server.yourServerName.tls ? 14 | 'https://' : 'http://') + 15 | config.server.yourServerName.host + 16 | ':' + 17 | config.server.yourServerName.port; 18 | 19 | const manifest = { 20 | 21 | server: { 22 | app: { 23 | config: config 24 | } 25 | }, 26 | 27 | connections: [ 28 | { 29 | host: config.server.yourServerName.host, 30 | port: config.server.yourServerName.port, 31 | labels: config.product.name 32 | } 33 | ], 34 | 35 | registrations: [ 36 | { 37 | plugin: { 38 | register: 'poop', 39 | options: config.poop 40 | } 41 | }, 42 | { 43 | plugin: { 44 | register: 'hapi-mongo-models', 45 | options: config.mongoModels 46 | } 47 | }, 48 | { 49 | plugin: { 50 | register: '../lib/index' 51 | } 52 | } 53 | ] 54 | }; 55 | 56 | module.exports = manifest; 57 | 58 | if (!module.parent) { 59 | 60 | const options = { 61 | relativeTo: path.join(__dirname, 'node_modules') 62 | }; 63 | 64 | var bootMessage = chalk.bgGreen.white(config.product.name + 65 | ' listening on') + ' ' + 66 | chalk.bgBlack.white.underline(config.server.yourServerName.uri) + 67 | chalk.bgBlue.white("\nENV: " + config.env); 68 | 69 | glue.compose(manifest, options, function serverComposer(err, server) { 70 | 71 | if (err) { 72 | throw err; 73 | } 74 | 75 | server.start(() => console.log(bootMessage)); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /test/example-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const glue = require('glue'); 5 | const Code = require('code'); 6 | const Lab = require('lab'); 7 | const http = require('http'); 8 | 9 | const manifest = require('../server'); 10 | const lab = exports.lab = Lab.script(); 11 | const config = require('../config').server.yourServerName; 12 | 13 | const describe = lab.describe; 14 | const it = lab.it; 15 | const before = lab.before; 16 | const after = lab.after; 17 | const expect = Code.expect; 18 | 19 | describe('example-test', () => { 20 | var server; 21 | 22 | before((done) => { 23 | const options = { 24 | relativeTo: path.join(__dirname, '..', 'node_modules') 25 | }; 26 | 27 | glue.compose(manifest, options, function serverComposer(err, composed) { 28 | 29 | if (err) { 30 | throw err; 31 | } 32 | 33 | server = composed; 34 | 35 | server.start((err) => { 36 | console.log('Test server started'); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | 42 | after((done) => { 43 | server.stop((err) => { 44 | console.log('Test server stopped'); 45 | done(); 46 | }); 47 | }); 48 | 49 | it('works with GET requests', (done) => { 50 | const options = { 51 | hostname: config.host, 52 | port: config.port, 53 | path: '/api/v1/example', 54 | method: 'GET', 55 | headers: { 56 | 'Content-Type': 'application/json', 57 | 'Authorization': 'token bar', 58 | } 59 | }; 60 | 61 | var response = ''; 62 | 63 | http.get(options, (res) => { 64 | res.setEncoding('utf-8'); 65 | res.on('data', (chunk) => { 66 | response += chunk; 67 | }); 68 | 69 | res.on('end', () => { 70 | expect(response).to.equal('GET example'); 71 | done(); 72 | }); 73 | 74 | expect(res.statusCode).to.equal(200); 75 | }); 76 | }); 77 | }); 78 | --------------------------------------------------------------------------------