├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── TLDR.md ├── examples ├── hapibell.js ├── hellogood.js ├── hellohapi.js ├── hellolog.js ├── helloparam.js ├── hellovalidate.js ├── public │ └── hello.js ├── socketio.js ├── staticfiles.js └── views │ └── index.html ├── makemehapi ├── 01-HELLO_HAPI.js ├── 02-ROUTES.js ├── 03-HANDLING.js ├── 04-DIRECTORIES.js ├── 05-VIEWS.js ├── 06-PROXIES.js ├── 07-HELPING.js ├── 08-STREAMS.js ├── 09-VALIDATION.js ├── 10-VALIDATION-USING-JOI-OBJECT.js ├── 11-UPLOADS.js ├── 12-COOKIES.js ├── foo │ └── bar │ │ └── baz │ │ └── file.html ├── helpers │ └── helper.js ├── index.html ├── input.txt └── templates │ ├── helper-index.html │ └── index.html ├── package.json └── test ├── coverage.html ├── hellovalidate.test.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | lcov.info 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | coverage.html 19 | 20 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # Compiled binary addons (https://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # Deployed apps should consider commenting this line out: 28 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 29 | node_modules 30 | .DS_Store 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | before_install: 5 | - pip install --user codecov 6 | after_success: 7 | - codecov --file coverage/lcov.info --disable search 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | _**Please read** our_ 2 | [**contribution guide**](https://github.com/dwyl/contributing) 3 | (_thank you_!) 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Happiness Is...](https://i.imgur.com/Df5Z18T.jpg) 2 | 3 | [![Build Status](https://travis-ci.org/dwyl/learn-hapi.png?branch=master)](https://travis-ci.org/dwyl/learn-hapi) 4 | [![codecov.io test coverage](https://codecov.io/github/dwyl/learn-hapi/coverage.svg?branch=master)](https://codecov.io/github/dwyl/learn-hapi?branch=master) 5 | [![Code Climate](https://codeclimate.com/github/dwyl/learn-hapi.png)](https://codeclimate.com/github/dwyl/learn-hapi) 6 | [![Dependencies](https://david-dm.org/dwyl/learn-hapi.png?theme=shields.io)](https://david-dm.org/dwyl/learn-hapi) 7 | [![devDependencies Status](https://david-dm.org/dwyl/learn-hapi/dev-status.svg)](https://david-dm.org/dwyl/learn-hapi?type=dev) 8 | [![NPM Version][npm-image]][npm-url] 9 | [![HitCount](https://hits.dwyl.com/dwyl/learn-hapi.svg?style=flat-square)](https://hits.dwyl.com/dwyl/learn-hapi) 10 | 11 | # Learn Hapi 12 | 13 | Happiness is learning how to use the [**Hapi.js**](https://hapijs.com/) (Node.js) web framework to 14 | _**build reliable/scalable apps faster**_. 15 | 16 | ## What is Hapi? 17 | 18 | Hapi is *the* framework for rapidly building RESTful & Real-Time web applications and services with Node.js.
19 | Whether you are building a very simple API 20 | for your website/mobile app or a large scale, cache heavy, 21 | secure e-commerce website, hapi has you covered. 22 | Hapi will help get your server developed quickly with its wide range 23 | of configurable options. 24 | 25 | ### *Watch* this intro/background to Hapi video: 26 | 27 | [![What is Hapi?](https://i.imgur.com/sZRoxdD.png)](https://youtu.be/BsyvnVOhp4U?t=3m50s "What is Hapi.js - Click to Watch!") 28 | 29 | *Most* people/teams that have _tried_ Hapi have _embraced_ Hapi to build *complete* web applications. But if you are only building a REST API (_e.g. for a mobile app_) 30 | please read: 31 | https://github.com/dwyl/learn-api-design 32 | 33 | ## _Why_ Hapi instead of XYZ framework? 34 | 35 | **Q**: I already know how to build REST APIs in `{framework-xyz}` why learn a *new* framework?
36 | **A**: If you are *happy* with your existing system & level of team productivity, 37 | stick with what you know. If not, learn [how to be] Hapi. 38 | (We have built Sites/APIs with both Express, Restify, Sails & Meteor and find Hapi has solved more 39 | "real world" problems and thus we end up writing less code. YMMV. See benefits below) 40 | 41 | **Q**: Hapi looks like quite a steep learning curve, 42 | how long will it take me to learn?
43 | **A**: You can get started *immediately* with the examples below, 44 | it will take _approximately **60 mins** to complete_ them all (after that add a couple of hours to read/learn further). 45 | The most important part is to ***try Hapi*** on a simple project to gain experience/confidence. 46 | 47 | ### Key Benefits 48 | 49 | - ***Performance*** - WalmartLabs are the guys who found/solved the 50 | [Node.js *CORE* Memory Leak](https://www.joyent.com/blog/walmart-node-js-memory-leak); 51 | they have developed Hapi following 52 | [Benchmark Driven Development](https://github.com/felixge/faster-than-c) 53 | and the result is a high-performance framework 54 | + ***Security*** - The *Lead* Developer of Hapi is [**Eran Hammer**](https://github.com/hueniverse) who was one of the original authors 55 | of the OAuth (Secure Authentication) Spec. He has built a security-focussed 56 | mindset into Hapi and reviews all code included in Hapi. Several members of the [Node Security Project](https://nodesecurity.io) are *core* contributors to 57 | Hapi which means there are many security-minded eyes on the code. 58 | - ***Scalability*** - they have focussed on *horizontal-scalability* 59 | and battle-tested the framework during [Black Friday](https://nodeup.com/fiftysix) 60 | (*holiday shopping busy day*) without incident. 61 | - **Mobile Optimised** (lightweight - built for mobile e-commerce) 62 | - **Plugin Architecture** - extend/add your own modules (good ecosystem) 63 | - ***DevOps Friendly*** - configuration based deployment and great stats/logging see: [#logging with good](https://github.com/dwyl/learn-hapi#logging-with-good) section below! 64 | - Built-in ***Caching*** (Redis, MongoDB or Memcached) 65 | - ***100% Test/Code Coverage*** (for the core) - *disciplined approach to code quality* 66 | + ***Testability*** - End-to-End testing is ***built-in*** to Hapi because 67 | it *includes* [**shot**](https://github.com/hapijs/shot) 68 | - **Key Functionality** is **Built-in** and there are *many* plugins for other 69 | features: https://hapijs.com/plugins 70 | 71 | ### _In-depth Comparison_ to Express.js 72 | 73 | @ethanmick wrote a detailed post on why _he_ prefers Hapi to Express: 74 | https://www.ethanmick.com/why-i-like-hapi-more-than-express/ --its worth a read. 75 | [PDF](https://github.com/dwyl/learn-hapi/files/502449/Why-I-like-Hapi-more-than-Express.pdf) 76 | 77 | ### _Beginner Friendly_ Examples/Apps to Learn From/With 78 | 79 | We have a few "_beginner_" example apps (with documentation & tests!) 80 | that will help you get started with something a bit more "real world": 81 | 82 | + Registration & Login (Basics): https://github.com/dwyl/hapi-login-example-postgres 83 | + Chat using Hapi, Redis & Socket.io: https://github.com/dwyl/hapi-socketio-redis-chat-example 84 | 85 | For a _list_ of examples see: https://github.com/dwyl?&query=example 86 | 87 | 88 | ## Who (_is using Hapi_) ? [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/dwyl/learn-hapi/issues) 89 | 90 | The list of teams using Hapi.js to build their node.js apps grows every day! 91 | See: https://hapijs.com/community 92 | 93 | > While you should _not_ make your decisions to use a given technology 94 | based on who _else_ is using it, you should be _aware_ that 95 | and if you need to answer the **question**: 96 | "***Who is already using this in Production?***" 97 | it's _really_ useful to have a good list. 98 | 99 | 100 | ## _How?!_ (_Dive In_!) 101 | 102 | ## Requirements 103 | 104 | - [x] A **computer** that can run [**Node.js**](https://nodejs.org/download/) Mac/Windows/Linux/Chromebook 105 | - [x] Access to the Internet (only required for installation) 106 | - [x] 60 minutes of time +/- 107 | 108 | ### Optional 109 | 110 | (_Not essential before you start, however_) You will _benefit_ from having: 111 | 112 | + [x] Basic JavaScript knowledge 113 | + [x] Basic experience of using node.js's `http` module. 114 | 115 | ## Make Me Hapi ("_Official_" _Beginner Workshop_) 116 | 117 | First thing you should do to get familiar with Hapi is work through the 118 | [makemehapi](https://nodeschool.io/#makemehapi) workshop.
119 | (_assumes some [node.js](https://nodeschool.io/#learn-you-node) prior 120 | knowledge but otherwise a gentle self-paced introduction_) 121 | 122 | _Note: makemehapi currently uses Hapi v16. Some major changes were introduced to Hapi in v17. [Differences between v16 and v17](#hapi-v16)_ 123 | 124 | Create a new folder on your local machine for your answers to **makemehapi**: 125 | 126 | ``` 127 | mkdir makemehapi && cd makemehapi 128 | ``` 129 | 130 | Install the workshop: 131 | 132 | ``` 133 | npm install -g makemehapi@latest 134 | ``` 135 | ( if it fails to install see: https://stackoverflow.com/questions/16151018/npm-throws-error-without-sudo ) 136 | 137 | Once its installed, start the tutorial with the following command: 138 | ``` 139 | makemehapi 140 | ``` 141 | 142 | _Try_ to complete all challenges. 143 | 144 | ![makemehapi complete](https://i.imgur.com/luXMDmg.png) 145 | 146 | If you get *stuck*, you can either _google_ for the specific error you are 147 | seeing or if you are not "getting" it, you can always look at my answers in the /**makemehapi** directory of this repository ***or*** 148 | _the_ "official" solutions 149 | in the **/makemehapi/exercises/{exercise-name}/solution** directory 150 | e.g: https://github.com/hapijs/makemehapi/tree/master/exercises/hello_hapi/solution 151 | 152 | or if you still don't get it, _**ask us**_: 153 | https://github.com/dwyl/learn-hapi/issues 154 | 155 |
156 | 157 | ## _Extended_ Examples 158 | 159 | For the rest of the tutorial we will cover the various 160 | plugins and features we have used in the Hapi.js ecosystem 161 | that will help you getting up-and-running with Hapi! 162 | 163 | ### Recap: Hello World in Hapi 164 | 165 | Once you have completed the **makemehapi** workshop, 166 | on your computer, create a new directory called "**hapiapp**". e.g: 167 | 168 | ```sh 169 | mkdir hapiapp && cd hapiapp 170 | ``` 171 | 172 | Type out (or copy-paste) this code into a file called **index.js** 173 | 174 | ```js 175 | const Hapi = require('hapi'); 176 | const server = new Hapi.Server({port: 3000}); // tell hapi which TCP Port to "listen" on 177 | 178 | server.route({ 179 | method: 'GET', // define the method this route will handle 180 | path: '/{yourname*}', // this is how you capture route parameters in Hapi 181 | handler: function(req, h) { // request handler method 182 | return 'Hello ' + req.params.yourname + '!'; // reply with text. 183 | } 184 | }); 185 | 186 | async function startServer() { 187 | await server.start() // start the Hapi server on your localhost 188 | console.log('Now Visit: http://localhost:' + server.info.port + '/YOURNAME'); 189 | } 190 | 191 | startServer(); 192 | 193 | module.exports = server; 194 | ``` 195 | Install Hapi: 196 | ``` 197 | npm init -y && npm install hapi --save 198 | ``` 199 | Run: 200 | ``` 201 | node . 202 | ``` 203 | 204 | Visit: http://localhost:3000/YOURNAME (in your browser) 205 | you should see something like: 206 | 207 | ![hello world in hapi](https://i.imgur.com/m9qcs17.png) 208 | 209 | 210 | ### Validation with Joi 211 | 212 | **Validation** is a fancy way of saying "checking" a value is 213 | the **type** / **format** and **length** you expect it to be. 214 | 215 | e.g. imagine you ask people to input their phone number 216 | and some joker enters letters instead of numbers. The validation 217 | will display a message to the person informing the data is incorrect. 218 | 219 | [**Joi**](https://github.com/hapijs/joi) is the validation library built by 220 | the same team as Hapi. 221 | Most people use Joi with Hapi, but given that it is a separate 222 | module, plenty of people use Joi independently; 223 | its well worth checking it out! 224 | 225 | An example: 226 | Type out (or copy-paste) this code into a file called **hellovalidate.js** 227 | 228 | ```js 229 | // Start this app from your command line with: node hellovalidate.js 230 | // then visit: http://localhost:3000/YOURNAME 231 | 232 | const Hapi = require('hapi'), 233 | Joi = require('joi'); 234 | 235 | const server = new Hapi.Server({ port: 3000 }); 236 | 237 | server.route({ 238 | method: 'GET', 239 | path: '/{yourname*}', 240 | config: { // validate will ensure YOURNAME is valid before replying to your request 241 | validate: { 242 | params: { 243 | yourname: Joi.string().min(2).max(40).alphanum().required() 244 | } 245 | }, 246 | handler: function (req, h) { 247 | return 'Hello '+ req.params.yourname + '!'; 248 | } 249 | } 250 | }); 251 | 252 | async function startServer() { 253 | await server.start(); // start the Hapi server on your localhost 254 | console.log('Now Visit: http://localhost:' + server.info.port + '/YOURNAME'); 255 | } 256 | 257 | startServer(); 258 | ``` 259 | 260 | Now try entering an _invalid_ name: http://localhost:3000/T 261 | You should see a **Validation Error**: 262 | 263 | ![Hapi Joi validation error](https://i.imgur.com/Dyhel2V.png) 264 | 265 | This might not _look_ like a "Friendly" Error message. 266 | But as we will see later, it provides all the information we need 267 | in our Client/App and we can display a more user-friendly error to people. 268 | 269 | [Joi](https://github.com/hapijs/joi) has many more useful validation methods. 270 | We will use a few of them later on when we build our example app. 271 | 272 | + Detailed example: https://github.com/hapijs/joi#example 273 | and https://vawks.com/blog/2014/03/22/the-joi-of-validation/ 274 | + Want _friendly_ error messages in your web app? 275 | see: [https://github.com/dwyl/**hapi-error**](https://github.com/dwyl/hapi-error) 276 | 277 | ### Testing with Lab 278 | 279 | If you're _new_ to Test Driven Development (**TDD**) read 280 | our ***Beginners' TTD Tutorial***: 281 | [https://github.com/dwyl/**learn-tdd**](https://github.com/dwyl/learn-tdd) (_first_) 282 | and then come _back_ to this tutorial! 283 | 284 | If you've done functional or unit testing in previous 285 | programming projects you will be at home with Lab. 286 | 287 | Lab borrows *heavily* from [Mocha](https://github.com/mochajs/mocha), 288 | so if you followed our 289 | [learn-mocha](https://github.com/dwyl/learn-mocha) tutorial this should all be familiar. 290 | 291 | (Using the code we wrote above in the **Validation with Joi** section with a minor addition) 292 | An example of testing with Lab: 293 | 294 | ```js 295 | const Lab = require("lab"); // load Lab module 296 | const lab = exports.lab = Lab.script(); //export test script 297 | const Code = require("code"); //assertion library 298 | const server = require("../examples/hellovalidate.js"); 299 | 300 | lab.experiment("Basic HTTP Tests", function() { 301 | // tests 302 | lab.test("GET /{yourname*} (endpoint test)", async function() { 303 | const options = { 304 | method: "GET", 305 | url: "/Timmy" 306 | }; 307 | // server.inject lets you simulate an http request 308 | const response = await server.inject(options); 309 | Code.expect(response.statusCode).to.equal(200); // Expect http response status code to be 200 ("Ok") 310 | Code.expect(response.result).to.have.length(12); // Expect result to be "Hello Timmy!" (12 chars long) 311 | await server.stop(); 312 | }); 313 | }); 314 | ``` 315 | First we create a *test suite* for our test **Lab.experiment** 316 | (the _first argument_ is the name of the test suite "Basic HTTP Tests") 317 | 318 | Next is a basic test that calls the only route we have `/{yourname}` 319 | in this case **GET /Timmy**. 320 | We expect to receive a **200** http status code and the response body to be 321 | the text "Hello Timmy!". 322 | 323 | 1. Create a **new directory** in your project called **test** 324 | 2. Create a **new file** called **test.js** in the **./test** dir 325 | 3. Type out or copy-paste the above code into **test.js** 326 | 4. Open your package.json file 327 | 5. Add a **scripts** section to the package.json file with the following: 328 | ``` 329 | "scripts": { 330 | "test": "lab -c" 331 | } 332 | ``` 333 | 6. Save the package.json file 334 | 7. run the **npm test** script from your command line to execute the tests 335 | 336 | The result should look something like this: 337 | 338 | Hapi testing with Lab 100% coverage 339 | 340 | Note how the test script has a `-c` (_coverage_) flag above 341 | this give us the **code coverage**. 342 | 343 | We have **100% code coverage** so we can move on to our next test/feature! 344 | 345 | > How do you think we would write a test for an error? 346 | > (hint: have a look inside ./test/test.js and see the second test :) 347 | 348 | ### Note on Testing: Tape is _Simpler_ than Lab+Code 349 | 350 | > *While* ***Lab*** *is really* ***Good*** *and is the "official" testing 351 | framework used by Hapi*, *we* ***prefer*** 352 | *the* ***simplicity*** 353 | > *of* [***tape***](https://github.com/substack/tape); 354 | > we find our tests are simpler to write/read/understand. #YMMV 355 | > Also we *prefer* to use a *separate* & *specialised* module for tracking 356 | test coverage: [istanbul](https://github.com/dwyl/learn-istanbul) 357 | which we find does a [better job](https://github.com/hapijs/lab/issues/401) at tracking coverage... 358 | 359 | The preceding `Lab` test can be re-written (*simplified*) in `Tape` as: 360 | 361 | ```js 362 | const test = require('tape'); 363 | const server = require("../index.js"); // our index.js from above 364 | 365 | test("Basic HTTP Tests - GET /{yourname*}", async function(t) { // t 366 | const options = { 367 | method: "GET", 368 | url: "/Timmy" 369 | }; 370 | // server.inject lets you similate an http request 371 | const response = await server.inject(options); 372 | t.equal(response.statusCode, 200); // Expect http response status code to be 200 ("Ok") 373 | t.equal(response.result.length, 12); // Expect result to be "Hello Timmy!" (12 chars long) 374 | await server.stop(); 375 | t.end(); // t.end() is required to end the test in tape 376 | }); 377 | ``` 378 | These tests are *functionally equivalent* in that they test *exactly* the 379 | same *outcome*. Decide for yourself which one you prefer for readability 380 | and maintainability in your projects. 381 | For our **Tape Tutorial** see: https://github.com/dwyl/learn-tape 382 | 383 | 384 | #### Related Links 385 | 386 | - Lab github module: https://github.com/hapijs/lab 387 | - Is TDD Dead? https://www.youtube.com/watch?v=z9quxZsLcfo (hint: no!) 388 | - Getting Started with HapiJS and Testing: https://blog.abcedmindedness.com/2014/10/getting-started-with-hapijs-and-testing.html (on hapi v8.0) 389 | 390 | ## Continuous Integration 391 | 392 | Making sure your code is working as you expect it to (over time). 393 | 394 | ### Integrating Hapi with Travis CI 395 | 396 | If you are new to Travis-CI or need a refresher see: https://github.com/dwyl/learn-travis 397 | 398 | We have Travis-CI enabled for all our hapi.js based projects: 399 | + https://github.com/dwyl/hapi-socketio-redis-chat-example 400 | + https://github.com/dwyl/hapi-auth-jwt2 401 | + https://github.com/dwyl/time 402 | + https://github.com/dwyl/api 403 | 404 | So if you need an example to follow, check out our repos! 405 | And as always, if you have _any questions, **ask**_! 406 | 407 | ### Error Handling with Boom 408 | 409 | [Boom](https://github.com/hapijs/boom) makes custom errors easier in Hapi. 410 | Imagine you have a page or item of content (photo, message, etc.) that 411 | you want to protect from public view (only show to someone who is logged in). 412 | 413 | First **install boom**: 414 | 415 | `npm install boom --save` 416 | 417 | Next write another test in ./test/**test.js** 418 | (If you aren't used to "Test First" - ***trust*** the process...) 419 | 420 | ```js 421 | lab.experiment("Authentication Required to View Photo", function() { 422 | // tests 423 | lab.test("Deny view of photo if unauthenticated /photo/{id*} ", async function() { 424 | const options = { 425 | method: "GET", 426 | url: "/photo/8795" 427 | }; 428 | // server.inject lets you simulate an http request 429 | const response = await server.inject(options); 430 | Code.expect(response.statusCode).to.equal(401); // Expect http response status code to be 200 ("Ok") 431 | Code.expect(response.result.message).to.equal("Please log-in to see that"); // (Don't hard-code error messages) 432 | }); 433 | }); 434 | ``` 435 | 436 | When you run `npm test` you will see a fail: 437 | 438 | Hapi auth test fail 439 | 440 | Next we want to make this test pass and we'll use Boom to get our custom error message. 441 | 442 | The wrong way of doing this is to explicitly hard-code the response for this route. 443 | The right way is to create a generic route which responds to any request for a photo with any id. 444 | And since we don't currently have any authentication set up, we ***mock*** (fake) it. 445 | (Don't worry we will get to the authentication in the next step...) 446 | 447 | ```js 448 | const Boom = require('boom'); 449 | server.route({ 450 | method: 'GET', 451 | path: '/photo/{id*}', 452 | config: { // validate will ensure `id` is valid before replying to your request 453 | validate: { params: { id: Joi.string().max(40).min(2).alphanum() } }, 454 | handler: function (req, h) { 455 | // until we implement authentication we are simply returning a 401: 456 | return Boom.unauthorized('Please log-in to see that'); 457 | // the key here is our use of the Boom.unauthorised method 458 | } 459 | } 460 | }); 461 | ``` 462 | 463 | Our test passes but the point was to show that returning errors 464 | with specific messages is *easy* with **Boom**. 465 | 466 | learn-hapi-clearer-boom-message 467 | 468 | Have a look at https://github.com/hapijs/boom for more error response options. 469 | We will be using these later as we build our app. 470 | Let's move on to authentication. 471 | 472 | > For a more _user-friendly_ approach to error-handling see: https://github.com/dwyl/hapi-error 473 | 474 | 475 | ### Logging with `good` 476 | 477 | Application logging can often be an _afterthought_ developers only implement 478 | _after_ they have a production bug which is crashing their API/App and 479 | they are scrambling to try and "debug" it. 480 | 481 | Thankfully, it Hapi has first-class support for logging with the `good` module. 482 | 483 | We have written a little example you can use to get started: 484 | [examples/hellogood.js](https://github.com/dwyl/learn-hapi/blob/master/examples/hellogood.js) 485 | 486 | Run it locally with `node examples/hellogood.js` then visit http://localhost:3000/hello/yourname in your browser. 487 | 488 | _Note: Good is not yet compatible with Hapi 17, so this code will only run if you are using v16. [See here for more details](#hapi-v16)_ 489 | 490 | You should expect to see something like this: 491 | ![learn-hapi-good-log-two-ops](https://cloud.githubusercontent.com/assets/194400/18990153/051440e8-8708-11e6-9337-bcc2ab067853.png) 492 | 493 | There are good examples including logging use http (e.g. to a 3rd party logging tool) 494 | in the Good repo: https://github.com/hapijs/good/tree/master/examples 495 | 496 | Again, if you have _any_ questions, [_ask_](https://github.com/dwyl/learn-hapi/issues) 497 | 498 | 499 | ### Authentication 500 | 501 | Authentication is the process of determining whether someone 502 | or something is, in fact, who or what it is declared to be. 503 | 504 | Authentication (or "Auth") is something many *novice* (*naive*?) 505 | developers attempt to write themselves. (I was once that kid... 506 | trust me, we have *bigger fish to fry*, use a well-written/tested library!) 507 | 508 | We have 4 options: 509 | 510 | 1. Google - If you are building an "enterprise" or "education" app 511 | which you know will be used in Google-enabled companies/schools we 512 | wrote a Hapi plugin: https://github.com/dwyl/hapi-auth-google which 513 | lets you include Google Login in your app in a few clear steps. The plugin uses the [***Official Google Node.js API Client***](https://github.com/google/google-api-nodejs-client) and is 514 | written to be as readable as possible for complete beginners. 515 | 2. EveryAuth - Specific to Connect/Express apps: https://github.com/bnoguchi/everyauth 516 | 3. [Passport.js](https://github.com/jaredhanson/passport) - ***100% Code Coverage*** and *many* excellent integrations https://passportjs.org/guide/providers/ 517 | 4. Bell - the 3rd party Login Hapi.js Plugin is *good* however we found it 518 | *lacking in documentation/usage examples*, which is why we wrote 519 | our own (*simpler*) Auth Plugin *specific* to our projects. 520 | see: [https://github.com/**dwyl**?query=**auth**](https://github.com/dwyl?utf8=%E2%9C%93&query=auth) 521 | 522 | 523 | #### Bell 524 | 525 | The go-to solution for 3rd party authentication in hapi is bell: https://github.com/hapijs/bell. 526 | There are a few good examples in the repo: https://github.com/hapijs/bell/tree/master/examples. 527 | 528 | 529 | ### Caching with Catbox 530 | 531 | Most apps don't _need_ caching from "Day 1" 532 | (_because you don't **know** upfront where your app's bottlenecks are going to be..._). 533 | 534 | But, once again, the team that brought you Hapi.js have _solved_ the problem of caching, 535 | see: https://github.com/hapijs/catbox/ and https://hapijs.com/tutorials/caching 536 | > We use Redis for blazing fast application and data caching. 537 | Hapi.js Catbox makes this very easy! 538 | 539 | 540 | 541 | ### Using Socket.io with Hapi for Real-Time Apps 542 | 543 | Using Socket.io with Hapi.js could _not_ be easier! 544 | To help you get started we've built a fully-functional chat application with tests (now [featured on the hapijs.com Resources page](https://hapijs.com/resources#Tutorials)), 545 | which demonstrates the power of Real-Time data-synching in your apps. 546 | 547 | > https://github.com/dwyl/hapi-socketio-redis-chat-example 548 | 549 | ## Hapi v16 550 | There were some major changes introduced to Hapi when version 17 was released. For a full list, see [the version 17 release notes](https://github.com/hapijs/hapi/issues/3658), but here are the major differences relevant to this guide: 551 | * Callbacks replaced with `async` functions. This means that instead of passing a callback to the function, and having that called when the function is finished, hapi functions return a promise that can either be resolved, or called synchronously with `await`. For more on `async` functions, see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) and [this guide](https://ponyfoo.com/articles/understanding-javascript-async-await) 552 | * `server.connection()` replaced with options passed directly into the server when it's created. A small change, but important to update. Before, we created our server with: 553 | ```js 554 | var server = new Hapi.server(); 555 | ``` 556 | and passed our options to: 557 | ```js 558 | server.connection({port: 8000}); 559 | ``` 560 | Now, we just pass our options straight away, and no longer need to call the connection method: 561 | ```js 562 | const server = new Hapi.server({port: 8000}); 563 | ``` 564 | * `reply()` interface replaced with a new lifecycle methods interface. You no longer have to call reply when sending a response from a handler. You can now just: 565 | ```js 566 | return "your reply"; 567 | ``` 568 | And the reply parameter to your handler has been replaced with a response toolkit (h) containing helpers from hapi core and your plugins. 569 | 570 | Not all of the hapi plugins have been updated to work with v17 yet (For example [Bell](https://github.com/hapijs/bell/issues/330), and [Good](https://github.com/hapijs/good/issues/568)), so be careful if you decide to upgrade an existing project. 571 | 572 | The previous version of this tutorial and code examples for Hapi 16 can be found here: https://github.com/dwyl/learn-hapi/tree/b58495ea002a9f3f8af8d183f6004d2b483f4591 573 | 574 | ## Please Suggest Improvements! [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/dwyl/learn-hapi/issues) 575 | 576 | If you want to extend this tutorial or simply request additional sections, 577 | open an issue on GitHub: https://github.com/dwyl/learn-hapi/issues 578 | 579 | 580 | ## Background Reading / Watching 581 | 582 | - GitHub Repo: https://github.com/hapijs/hapi (has documentation) 583 | - An ecosystem of tools and best practices for the working hapijs developer (up-to-date with last version of hapi): https://hapipal.com/ https://hapipal.com/getting-started 584 | 585 | - Restify vs Express performance: https://stackoverflow.com/questions/17589178/why-should-i-use-restify 586 | - REST API in Express: https://pixelhandler.com/posts/develop-a-restful-api-using-nodejs-with-express-and-mongoose 587 | - Hapi API Reference: https://github.com/hapijs/hapi/blob/master/API.md 588 | 589 | 590 | ### Video Intro 591 | 592 | - Hapi.js and why it's awesome: https://www.youtube.com/watch?v=ZI9wXL-add0&t=2m5s 593 | - Hapi overview: https://www.youtube.com/watch?v=Recv7vR8ZlA (old version but concepts still relevant) 594 | 595 | ### Tutorials 596 | 597 | - Hapi Boilerplate app: https://github.com/poeticninja/hapi-ninja [updated for hapi 8.0] 598 | - Building APIs with Hapi and MongoDB: https://speakerdeck.com/donnfelker/building-web-apis-with-hapi-dot-js-and-mongodb-mongoose 599 | - Repo for the above speakerdeck: https://github.com/donnfelker/hapi-mongodb-example 600 | - Micro-tutorial: https://github.com/hapijs/makemehapi 601 | - https://stackoverflow.com/questions/21455076/hapi-and-node-js-to-create-a-rest-api-server 602 | - Hapi + Twilio (sms): https://code.tutsplus.com/tutorials/creating-a-node-web-app-with-hapi-and-twilio-integration--cms-20769 603 | - Authentication: https://github.com/hapijs/hapi-auth-cookie 604 | - A few examples: https://github.com/andyroyle/hapi-examples 605 | - More examples: https://github.com/wpreul/hapikc (*very old* version of Hapi!) 606 | - BDD with Hapi and Lab: https://gist.github.com/thebillkidy/10a11fed1bf61d04c3c5 (*old* version of Hapi!) 607 | + If you like React.js checkout Mullet.js (Hapi.js + React.js): 608 | https://mullet.io/ + https://github.com/lynnaloo/mullet 609 | + If you have an *existing* ***Express*** App and are thinking of 610 | migrating to Hapi, read: https://matt-harrison.com/moving-from-express-to-hapi-js/ 611 | 612 | Selected StackOverflow Questions & Answers: 613 | - https://stackoverflow.com/questions/22934340/hapi-js-api-authentication 614 | see: https://stackoverflow.com/a/33877047/1148249 (*answer*) 615 | - https://stackoverflow.com/questions/22985392/how-do-you-make-a-hapi-js-plugin-module 616 | see: https://stackoverflow.com/a/25135343/1148249 (*answer*) 617 | - https://stackoverflow.com/questions/18343509/hapi-js-with-socket-io-where-is-socket-io-js 618 | see: https://stackoverflow.com/a/33876615/1148249 (*answer* 619 | 620 | 621 | [npm-image]: https://img.shields.io/npm/v/hapi.svg?style=flat 622 | [npm-url]: https://npmjs.org/package/learn-hapi 623 | -------------------------------------------------------------------------------- /TLDR.md: -------------------------------------------------------------------------------- 1 | # Note: Hapi was built by Walmart 2 | 3 | ![Hapi is Made by Walmart](https://i.imgur.com/bxYADdu.png) 4 | 5 | Hapi was (_originally_) built by (_the fantastic team of people assembled by [@hueniverse](https://github.com/hueniverse)_) [@WalmartLabs](https://en.wikipedia.org/wiki/@WalmartLabs) 6 | for Walmart. 7 | 8 | [Walmart](https://en.wikipedia.org/wiki/Walmart) is *by far* the most successful 9 | retailer in the world and they have achieved their success (*in part*) by 10 | investing heavily in their *technological competitive advantage*. 11 | 12 | If you are not keen on Walmart for any of 13 | [these](https://en.wikipedia.org/wiki/Criticism_of_Walmart) reasons, 14 | at least *consider* the fact that they have open-sourced their full 15 | node stack to allow others to benefit from their hard work. 16 | 17 | I took the time to read *extensively* about Walmart as part of my 18 | Retail course at University 19 | see: [History of Walmart](https://en.wikipedia.org/wiki/History_of_Walmart) 20 | and [In Sam We Trust](https://www.bizsum.com/summaries/sam-we-trust). 21 | The fact is that 22 | [Sam Walton](https://en.wikipedia.org/wiki/Sam_Walton) 23 | achieved *much* of his success through 24 | investing in *technology* 25 | (Barcodes, EPOS, Satellite Uplink for faster IT Systems and Logistics Tracking, etc) 26 | to drive cost savings and passed those savings on to the 27 | customers where other retailers got left behind with their paper-based 28 | "*it still works, why change?*" approach. 29 | 30 | Since Sam's passing the Walmart leadership has compromised its ethics 31 | in favor of maximising profits. This documented in: 32 | [The High Cost Of Low Price](https://www.youtube.com/results?search_query=Wal*Mart+-+The+High+Cost+Of+Low+Price) 33 | and [The Wal-Mart Effect](https://en.wikipedia.org/wiki/The_Wal-Mart_Effect) 34 | 35 | While I think we can/should continue send a 36 | [*clear message*](https://en.wikipedia.org/wiki/Dollar_voting) 37 | to [Bentonville](https://en.wikipedia.org/wiki/Walmart#Corporate_affairs) 38 | by preferring to spend our $¥£€ at Local & Fairtrade retailers where ever possible, 39 | we can still *use* the ***best-in-class*** code the *fantastic engineers* 40 | have built (to meet their *vast* supply-chain and e-commerce needs and 41 | open-sourced) to craft our own software products/projects. 42 | 43 | Using the transport analogy, I don't *like* using fossil fuels to get from A-to-B 44 | because of the CO2 emissions. But I'm *pragmatic* about how I travel 45 | the [thousand miles](https://www.wolframalpha.com/input/?i=distance+from+London+to+Lisbon) 46 | to visit my family twice a year. I could do two weeks by horse-and-cart, 47 | two days by train or two hours by plane each way. Which option do you take...? 48 | By chosing Hapi you are opting for the jet engine. 49 | 50 | Make up your own mind on whether you feel that using code written for Walmart 51 | goes against your ethics.
52 | If you find a *better* open source Node.js stack that fits your needs, 53 | *please* ***[tell us](https://twitter.com/nelsonic)*** about it! 54 | -------------------------------------------------------------------------------- /examples/hapibell.js: -------------------------------------------------------------------------------- 1 | /** 2 | Bell is currently not compatible with Hapi v17, 3 | so this example is using Hapi v16 4 | **/ 5 | 6 | var Hapi = require('hapi'); 7 | var server = new Hapi.Server(); 8 | server.connection({ 9 | port: 3000 10 | }); 11 | 12 | // Register bell with the server 13 | server.register(require('bell'), function (err) { 14 | 15 | // Declare an authentication strategy using the bell scheme 16 | // with the name of the provider, cookie encryption password, 17 | // and the OAuth client credentials. 18 | server.auth.strategy('twitter', 'bell', { 19 | provider: 'twitter', 20 | password: 'cookie_encryption_password', 21 | clientId: 'my_twitter_client_id', 22 | clientSecret: 'my_twitter_client_secret', 23 | isSecure: false // Terrible idea but required if not using HTTPS 24 | }); 25 | 26 | // Use the 'twitter' authentication strategy to protect the 27 | // endpoint handling the incoming authentication credentials. 28 | // This endpoints usually looks up the third party account in 29 | // the database and sets some application state (cookie) with 30 | // the local application account information. 31 | server.route({ 32 | method: ['GET', 'POST'], // Must handle both GET and POST 33 | path: '/login', // The callback endpoint registered with the provider 34 | config: { 35 | auth: 'twitter', 36 | handler: function (request, reply) { 37 | 38 | // Perform any account lookup or registration, setup local session, 39 | // and redirect to the application. The third-party credentials are 40 | // stored in request.auth.credentials. Any query parameters from 41 | // the initial request are passed back via request.auth.credentials.query. 42 | return reply.redirect('/home'); 43 | } 44 | } 45 | }); 46 | 47 | server.start(); 48 | }); 49 | -------------------------------------------------------------------------------- /examples/hellogood.js: -------------------------------------------------------------------------------- 1 | /** 2 | Good is currently not compatible with Hapi v17, 3 | so this example is using Hapi v16 4 | **/ 5 | 6 | /** 7 | Start this app from your command line with: 8 | node examples/hellogood.js 9 | then visit: http://localhost:3000/hello/YOURNAME 10 | */ 11 | 12 | var Hapi = require('hapi'); 13 | 14 | var server = new Hapi.Server(); 15 | 16 | server.connection({ 17 | host: '0.0.0.0', 18 | port: 3000 19 | }); 20 | 21 | server.route({ 22 | method: 'GET', 23 | path: '/hello/{name*}', 24 | handler: function(request, reply) { 25 | // log the request 26 | request.log(['name'], request.params.name + ' (request.log)'); 27 | // use request.log instead of console.log: 28 | request.log('info', {my: 'object', has: 'many', props: { hello: request.params.name } }); 29 | // server.log(['name'], request.params.name + ' requested / ' + '(server.log)'); 30 | return reply('Hello ' + request.params.name); 31 | 32 | } 33 | }) 34 | 35 | const options = { 36 | ops: { 37 | interval: 30000 // reporting interval (30 seconds) 38 | }, 39 | reporters: { 40 | myConsoleReporter: [{ 41 | // good-squeeze allows filtering events based on the `good` event options 42 | // @see https://github.com/hapijs/good/blob/master/API.md#event-types 43 | module: 'good-squeeze', // https://github.com/hapijs/good-squeeze 44 | name: 'Squeeze', 45 | // @example "Log everything" 46 | args: [{ log: '*', error: '*', response: '*', request: '*', ops: '*' }] 47 | // @example "Log only request logs with a certain tag": 48 | // args: [{ request: ['name'] }] 49 | }, { 50 | module: 'good-console' 51 | }, 'stdout'] 52 | } 53 | }; 54 | 55 | server.register({ 56 | register: require('good'), 57 | options, 58 | }, function (err) { 59 | 60 | if (err) { 61 | return console.error(err); 62 | } 63 | 64 | server.start(function () { 65 | console.log('Now Visit: http://localhost:' + server.info.port + '/YOURNAME'); 66 | console.dir(server.info); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /examples/hellohapi.js: -------------------------------------------------------------------------------- 1 | // Start the app from your command line with: node examples/hellohapi.js 2 | // then visit: http://localhost:8000 in your browser 3 | 4 | const Hapi = require('hapi'); 5 | const server = new Hapi.Server({ 6 | host: '0.0.0.0', 7 | port: Number(process.argv[2] || 8000) 8 | }); 9 | 10 | server.route({ 11 | method: 'GET', 12 | path: '/', 13 | handler: function(req, h) { 14 | return 'Hello Hapi'; 15 | } 16 | }); 17 | 18 | async function startServer() { 19 | await server.start(); // boots your server 20 | console.log('Now Visit: http://localhost:'+server.info.port); 21 | } 22 | -------------------------------------------------------------------------------- /examples/hellolog.js: -------------------------------------------------------------------------------- 1 | /** 2 | Start this app from your command line with: 3 | node examples/hellolog.js 4 | then visit: http://localhost:3000/YOURNAME 5 | */ 6 | const Hapi = require('hapi'); 7 | 8 | const server = new Hapi.Server({ 9 | debug: { 10 | request: ["received"] // logs all requests to stdout 11 | }, 12 | host: '0.0.0.0', 13 | port: 8000 14 | }); 15 | 16 | 17 | server.route({ 18 | method: 'GET', 19 | path: '/{name*}', 20 | handler: function(request, h){ 21 | // log before seding a reply 22 | server.log(["test"], request.params.name + " requested the hello page!"); 23 | 24 | return 'Hai ' + request.params.name; 25 | } 26 | }) 27 | 28 | async function startServer() { 29 | await server.start(); 30 | console.log('Now Visit: http://localhost:' + server.info.port + '/YOURNAME'); 31 | console.dir(server.info); 32 | } 33 | 34 | startServer(); 35 | 36 | // Listen for events of type 'log' 37 | server.events.on("log", function(event, tags) { 38 | const tagsJoined = Object.keys(tags).join(); 39 | const msg = event.data; 40 | console.log(tagsJoined+" | "+msg); 41 | }); 42 | -------------------------------------------------------------------------------- /examples/helloparam.js: -------------------------------------------------------------------------------- 1 | /** 2 | Start this app from your command line with: 3 | node examples/helloparam.js 4 | then visit: http://localhost:3000/YOURNAME 5 | */ 6 | const Hapi = require('hapi'); 7 | const server = new Hapi.Server({ 8 | host: '0.0.0.0', 9 | port: 3000 10 | }); 11 | 12 | server.route({ 13 | method: 'GET', 14 | path: '/{p*}', 15 | handler: function(req, h){ 16 | return 'Hello ' + req.params.p; 17 | } 18 | }) 19 | 20 | async function startServer() { 21 | await server.start(); 22 | console.log('Now Visit: http://localhost:' + server.info.port + '/YOURNAME'); 23 | } 24 | -------------------------------------------------------------------------------- /examples/hellovalidate.js: -------------------------------------------------------------------------------- 1 | // Start this app from your command line with: node examples/hellovalidate.js 2 | // then visit: http://localhost:3000/YOURNAME 3 | 4 | const Hapi = require('hapi'); 5 | const Joi = require('joi'); 6 | const Boom = require('boom'); 7 | const port = 3000; // process.env.PORT || 3000; // allow port to be set by environment 8 | 9 | const server = new Hapi.Server({ port: port }); 10 | 11 | server.route({ 12 | method: ['GET', 'POST'], 13 | path: '/{name*}', 14 | config: { // validate will ensure YOURNAME is valid before replying to your request 15 | validate: { params: Joi.object().keys({ name: Joi.string().max(40).min(2).alphanum() }) }, 16 | handler: function (request, h) { 17 | return 'Hai '+ request.params.name + '!'; 18 | } 19 | } 20 | }); 21 | 22 | server.route({ 23 | method: 'GET', 24 | path: '/photo/{id*}', 25 | config: { // validate will ensure YOURNAME is valid before replying to your request 26 | validate: { params: { id: Joi.string().max(40).min(2).alphanum() } }, 27 | handler: function (request, h) { 28 | // until we implement authentication we are simply returning a 401: 29 | return Boom.unauthorized('Please log-in to see that'); 30 | } 31 | } 32 | }); 33 | 34 | module.exports = server; 35 | -------------------------------------------------------------------------------- /examples/public/hello.js: -------------------------------------------------------------------------------- 1 | console.log('Hello World!'); 2 | -------------------------------------------------------------------------------- /examples/socketio.js: -------------------------------------------------------------------------------- 1 | // see: https://github.com/dwyl/hapi-socketio-redis-chat-examples 2 | -------------------------------------------------------------------------------- /examples/staticfiles.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const Hapi = require('hapi'); 3 | const port = process.env.PORT || 5000; 4 | const server = new Hapi.Server({ port: port }); 5 | 6 | const plugins = [ 7 | require('inert'), // serve static content 8 | require('vision') // views 9 | ] 10 | 11 | async function startServer() { 12 | 13 | await server.register(plugins); 14 | 15 | server.views({ 16 | engines: { 17 | html: require('handlebars') 18 | }, 19 | path: Path.join(__dirname, 'views') 20 | }); 21 | 22 | server.route([ 23 | { 24 | path: '/', 25 | method: 'GET', 26 | config: { 27 | auth: false, 28 | handler: function(request, h) { 29 | return h.view("index"); 30 | } 31 | } 32 | }, 33 | { 34 | method: 'GET', 35 | path: '/public/{param*}', 36 | handler: { 37 | directory: { 38 | path: Path.normalize(__dirname + '/public') 39 | } 40 | } 41 | } 42 | ]); 43 | 44 | await server.start(); 45 | console.log('Static Server Listening on : http://127.0.0.1:' +port); 46 | } 47 | 48 | startServer(); 49 | 50 | module.exports = server; 51 | -------------------------------------------------------------------------------- /examples/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Try Serving Static Content in Hapi.js 9.0.1 5 | 6 | 7 | 8 | 9 |

Open Your (Browser's) Developer Console...

10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /makemehapi/01-HELLO_HAPI.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var server = new Hapi.Server(); 3 | 4 | server.connection({ 5 | host: 'localhost', 6 | port: Number(process.argv[2] || 8080) 7 | }); 8 | 9 | function hellohandler(request, reply) { 10 | reply("Hello Hapi"); 11 | } 12 | 13 | server.route({path: '/', method:'GET', handler: hellohandler}); 14 | 15 | server.start(); 16 | -------------------------------------------------------------------------------- /makemehapi/02-ROUTES.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var server = new Hapi.Server(); 3 | 4 | server.connection({ 5 | host: 'localhost', 6 | port: Number(process.argv[2] || 8080) 7 | }); 8 | 9 | function routehandler(request, reply) { 10 | reply('Hello ' + encodeURIComponent(request.params.name)); 11 | } 12 | 13 | server.route({path: '/{name*}', method:'GET', handler: routehandler}); 14 | 15 | server.start(); 16 | -------------------------------------------------------------------------------- /makemehapi/03-HANDLING.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var server = new Hapi.Server(); 3 | 4 | server.connection({ 5 | host: 'localhost', 6 | port: Number(process.argv[2] || 8080) 7 | }); 8 | 9 | server.route({path: '/{name*}', method:'GET', 10 | handler: { 11 | file: "index.html" 12 | } 13 | }); 14 | 15 | server.start(); 16 | -------------------------------------------------------------------------------- /makemehapi/04-DIRECTORIES.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var server = new Hapi.Server(); 3 | 4 | server.connection({ 5 | host: 'localhost', 6 | port: Number(process.argv[2] || 8080) 7 | }); 8 | 9 | function hellohandler(request, reply) { 10 | reply('Hello ' + encodeURIComponent(request.params.name)); 11 | } 12 | 13 | server.route({path: '/{name*}', method:'GET', 14 | handler: { 15 | directory: { path: __dirname } 16 | } 17 | }); 18 | 19 | server.start(); 20 | -------------------------------------------------------------------------------- /makemehapi/05-VIEWS.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var Path = require('path'); 3 | var server = new Hapi.Server(); 4 | 5 | server.connection({ 6 | host: 'localhost', 7 | port: Number(process.argv[2] || 8080) 8 | }); 9 | 10 | server.route({path: '/{name*}', method:'GET', 11 | handler: { 12 | view: "index.html" 13 | } 14 | }); 15 | 16 | server.views({ 17 | engines: { 18 | html: require('handlebars') 19 | }, 20 | path: Path.join(__dirname, 'templates') 21 | }); 22 | 23 | server.start(); 24 | -------------------------------------------------------------------------------- /makemehapi/06-PROXIES.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var server = new Hapi.Server(); 3 | 4 | server.connection({ 5 | host: 'localhost', 6 | port: Number(process.argv[2] || 8080) 7 | }); 8 | 9 | server.route({path: '/proxy', method:'GET', 10 | handler: { 11 | proxy: { 12 | host: '127.0.0.1', 13 | port: 65535 14 | } 15 | } 16 | }); 17 | 18 | server.start(); 19 | -------------------------------------------------------------------------------- /makemehapi/07-HELPING.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var Path = require('path'); 3 | var server = new Hapi.Server(); 4 | 5 | server.connection({ 6 | host: 'localhost', 7 | port: Number(process.argv[2] || 8080) 8 | }); 9 | 10 | server.route({path: '/', method:'GET', 11 | handler: { 12 | view: "helper-index.html" 13 | } 14 | }); 15 | 16 | server.views({ 17 | engines: { 18 | html: require('handlebars') 19 | }, 20 | path: Path.join(__dirname, 'templates'), 21 | helpersPath: Path.join(__dirname, 'helpers') 22 | }); 23 | 24 | server.start(); 25 | -------------------------------------------------------------------------------- /makemehapi/08-STREAMS.js: -------------------------------------------------------------------------------- 1 | var Fs = require('fs'); 2 | var Hapi = require('hapi'); 3 | var Path = require('path'); 4 | var Rot13 = require('rot13-transform'); 5 | 6 | var server = new Hapi.Server(); 7 | 8 | server.connection({ 9 | host: 'localhost', 10 | port: Number(process.argv[2] || 8080) 11 | }); 12 | 13 | server.route({ 14 | method: 'GET', 15 | path: '/', 16 | config: { 17 | handler: function (request, reply) { 18 | var thisfile = Fs.createReadStream(Path.join(__dirname, 'input.txt')); 19 | reply(thisfile.pipe(Rot13())); 20 | } 21 | } 22 | }); 23 | 24 | server.start(); 25 | -------------------------------------------------------------------------------- /makemehapi/09-VALIDATION.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var Joi = require('joi'); 3 | var server = new Hapi.Server(); 4 | 5 | server.connection({ 6 | host: 'localhost', 7 | port: Number(process.argv[2] || 8080) 8 | }); 9 | 10 | function myHandler(request, reply) { 11 | reply('Hello Joi!'); 12 | } 13 | 14 | var routeConfig = { 15 | path: '/a/path/{with}/{parameters}', 16 | method: 'GET', 17 | handler: myHandler, 18 | config: { 19 | validate: { 20 | params: { 21 | with: Joi.string().required(), 22 | parameters: Joi.string().required() 23 | } 24 | } 25 | } 26 | } 27 | 28 | server.route(routeConfig); 29 | 30 | server.start(); 31 | -------------------------------------------------------------------------------- /makemehapi/10-VALIDATION-USING-JOI-OBJECT.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var Joi = require('joi'); 3 | var server = new Hapi.Server(); 4 | 5 | server.connection({ 6 | host: 'localhost', 7 | port: Number(process.argv[2] || 8080) 8 | }); 9 | 10 | function myHandler(request, reply) { 11 | reply('login successful'); 12 | } 13 | 14 | var routeConfig = { 15 | path: '/login', 16 | method: 'POST', 17 | config: { 18 | handler: myHandler, 19 | validate: { 20 | payload: Joi.object({ 21 | username: Joi.string(), 22 | password: Joi.string().alphanum(), 23 | accessToken: Joi.string().alphanum(), 24 | birthyear: Joi.number().integer().min(1900).max(2013), 25 | email: Joi.string().email() 26 | }) 27 | .options({allowUnknown: true}) 28 | // .with('username', 'birthyear') 29 | .without('password', 'accessToken') 30 | } 31 | } 32 | } 33 | 34 | server.route(routeConfig); 35 | 36 | server.start(); 37 | -------------------------------------------------------------------------------- /makemehapi/11-UPLOADS.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var server = new Hapi.Server(); 3 | 4 | server.connection({ 5 | port: Number(process.argv[2] || 8080), 6 | host: 'localhost' 7 | }); 8 | 9 | server.route({ 10 | method: 'POST', 11 | path: '/upload', 12 | config: { 13 | handler: function(request, reply) { 14 | var body = ''; 15 | request.payload.file.on('data', function(data) { 16 | body += data; 17 | }); 18 | request.payload.file.on('end', function() { 19 | var result = { 20 | description: request.payload.description, 21 | file: { 22 | data: body, 23 | filename: request.payload.file.hapi.filename, 24 | headers: request.payload.file.hapi.headers 25 | } 26 | }; 27 | reply(JSON.stringify(result)); 28 | }); 29 | }, 30 | payload: { 31 | output: 'stream', 32 | parse: true, 33 | allow: 'multipart/form-data' 34 | } 35 | } 36 | }); 37 | 38 | server.start(); 39 | -------------------------------------------------------------------------------- /makemehapi/12-COOKIES.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var server = new Hapi.Server(); 3 | 4 | server.connection({ 5 | host: 'localhost', 6 | port: Number(process.argv[2] || 8080) 7 | }); 8 | 9 | server.state('session', { 10 | path: '/{path*}', 11 | encoding: 'base64json', 12 | ttl: 10, 13 | domain: 'localhost' 14 | }); 15 | 16 | server.route( 17 | { 18 | method: 'GET', 19 | path: '/set-cookie', 20 | handler: function (request, reply) { 21 | return reply({ 22 | message : 'success' 23 | }).state('session', { 24 | key : 'makemehapi' 25 | }); 26 | }, 27 | config: { 28 | state: { 29 | parse: true, 30 | failAction: 'log' 31 | } 32 | } 33 | } 34 | ); 35 | 36 | server.route( 37 | { 38 | method: 'GET', 39 | path: '/check-cookie', 40 | handler: function (request, reply) { 41 | var session = request.state.session; 42 | var result; 43 | if (session) { 44 | result = { 45 | user : 'hapi' 46 | }; 47 | } else { 48 | result = new Hapi.error.unauthorized('Missing authentication'); 49 | } 50 | reply(result); 51 | } 52 | } 53 | ); 54 | 55 | server.start(); 56 | -------------------------------------------------------------------------------- /makemehapi/foo/bar/baz/file.html: -------------------------------------------------------------------------------- 1 | 2 | Hello Directories 3 | 4 | Hello Directories 5 | 6 | 7 | -------------------------------------------------------------------------------- /makemehapi/helpers/helper.js: -------------------------------------------------------------------------------- 1 | module.exports = function(context) { 2 | var query = context.data.root.query; 3 | return query.name + query.suffix; 4 | } 5 | -------------------------------------------------------------------------------- /makemehapi/index.html: -------------------------------------------------------------------------------- 1 | 2 | Hello Handling 3 | 4 | Hello Handling 5 | 6 | 7 | -------------------------------------------------------------------------------- /makemehapi/input.txt: -------------------------------------------------------------------------------- 1 | The Pursuit of Hapi-ness 2 | -------------------------------------------------------------------------------- /makemehapi/templates/helper-index.html: -------------------------------------------------------------------------------- 1 | 2 | Hello {{helper}} 3 | 4 | Hello {{helper}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /makemehapi/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | Hello {{query.name}} 3 | 4 | Hello {{query.name}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-hapi", 3 | "version": "17.2.0", 4 | "description": "Learn to build Web Applications & APIs using Hapi.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/lab/bin/lab -c && ./node_modules/lab/bin/lab -r lcov -o coverage/lcov.info", 8 | "start": "node index.js", 9 | "coverage": "./node_modules/lab/bin/lab -r html -o ./coverage/index.html && open ./coverage/index.html" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/dwyl/learn-hapi.git" 14 | }, 15 | "keywords": [ 16 | "learn", 17 | "hapi", 18 | "how-to", 19 | "introduction", 20 | "tutorial" 21 | ], 22 | "author": "@nelsonic & Friends!", 23 | "license": "GPL-2.0", 24 | "bugs": { 25 | "url": "https://github.com/dwyl/learn-hapi/issues" 26 | }, 27 | "homepage": "https://github.com/dwyl/learn-hapi", 28 | "dependencies": { 29 | "bell": "^10.0.0", 30 | "boom": "^7.1.1", 31 | "good": "^8.1.0", 32 | "good-console": "^8.0.0", 33 | "good-squeeze": "^5.0.2", 34 | "handlebars": "^4.0.11", 35 | "hapi": "^18.1.0", 36 | "inert": "^5.1.0", 37 | "joi": "^14.0.0", 38 | "socket.io": "^2.0.4", 39 | "vision": "^5.3.1" 40 | }, 41 | "devDependencies": { 42 | "code": "^5.2.0", 43 | "lab": "^18.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/coverage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tests & Coverage 5 | 89 | 537 | 538 | 539 | 548 |
549 |

Test Report

550 |
551 |
0
552 |
0
553 |
3
554 |
57
555 |
556 |
557 | 558 | 559 |
560 | 561 | 562 |
563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 577 | 578 | 579 | 580 | 581 | 584 | 585 | 586 | 587 | 588 | 591 | 592 | 593 | 594 |
IDTitleDuration (ms)
1Basic HTTP Tests Main endpoint /{yourname*} 575 | 576 | 41
2Basic HTTP Tests creating valid user 582 | 583 | 11
3Authentication Required to View Photo Deny view of photo if unauthenticated /photo/{id*} 589 | 590 | 3
595 | 596 |
597 |

Code Coverage Report

598 |
599 |
100%
600 |
33
601 |
33
602 |
0
603 |
604 |
605 |
606 |

index.js

607 |
608 |
100%
609 |
33
610 |
33
611 |
0
612 |
613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 |
LineLintHitsSource
1// Start this app from your command line with: node hellovalidate.js
2// then visit: http://localhost:3000/YOURNAME
3
41var Hapi = require('hapi');
51var Joi = require('joi');
61var Boom = require('boom');
71var port = 3000; // process.env.PORT || 3000; // allow port to be set by environment
8
91var server = new Hapi.Server();
101server.connection({ port: port });
11
121server.route({
13 method: ['GET', 'POST'],
14 path: '/{name*}',
15 config: { // validate will ensure YOURNAME is valid before replying to your request
16 validate: { params: { name: Joi.string().max(40).min(2).alphanum() } },
17 handler: function (request, reply) {
181 reply('Hai '+ request.params.name + '!');
19 }
20 }
21});
22
231server.route({
24 method: 'GET',
25 path: '/photo/{id*}',
26 config: { // validate will ensure YOURNAME is valid before replying to your request
27 validate: { params: { id: Joi.string().max(40).min(2).alphanum() } },
28 handler: function (request, reply) {
29 // until we implement authentication we are simply returning a 401:
301 reply(Boom.unauthorized('Please log-in to see that'));
31 }
32 }
33});
34
351server.start(function() {
361 console.log('Now Visit: http://localhost:' + port + '/YOURNAME');
37});
38
391module.exports = server;
40
865 |
866 |
867 |
868 |

Linting Report

869 | Nothing to show here, linting is disabled. 870 |
871 | 872 | -------------------------------------------------------------------------------- /test/hellovalidate.test.js: -------------------------------------------------------------------------------- 1 | const Code = require('code'); // assertion library 2 | const Lab = require('lab'); 3 | const lab = exports.lab = Lab.script(); 4 | 5 | const server = require("../examples/hellovalidate"); // require ../index.js 6 | 7 | lab.experiment("Joi Validation Test", function() { 8 | // tests 9 | lab.test("GET /j should fail due to insufficient name length", async function() { 10 | const options = { 11 | method: "GET", 12 | url: "/j" 13 | }; 14 | const response = await server.inject(options); 15 | Code.expect(response.statusCode).to.equal(400); // Expect http response status code to be 400 ("Bad Request") 16 | await server.stop(); 17 | }); 18 | 19 | lab.test("GET /jo should pass", async function() { 20 | const options = { 21 | method: "GET", 22 | url: "/jo" 23 | }; 24 | const response = await server.inject(options); 25 | Code.expect(response.statusCode).to.equal(200); // Expect http response status code to be 200 ("Ok") 26 | Code.expect(response.result).to.have.length(7); // Expect result to be "Hai jo!" (7 chars long) 27 | await server.stop(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const Code = require('code'); // assertion library 2 | const Lab = require('lab'); 3 | const lab = exports.lab = Lab.script(); 4 | 5 | const server = require("../examples/hellovalidate.js"); 6 | 7 | lab.experiment("Basic HTTP Tests", function() { 8 | // tests 9 | lab.test("Main endpoint /{yourname*} ", async function() { 10 | const options = { 11 | method: "GET", 12 | url: "/Timmy" 13 | }; 14 | // server.inject lets you similate an http request 15 | const response = await server.inject(options) 16 | Code.expect(response.statusCode).to.equal(200); // Expect http response status code to be 200 ("Ok") 17 | Code.expect(response.result).to.have.length(10); // Expect result to be "Hai Timmy!" (10 chars long) 18 | }); 19 | 20 | // we expect this test to return 400 (validation error) 21 | lab.test("creating valid user", async function() { 22 | const options = { 23 | method: "GET", 24 | url: "/T" 25 | }; 26 | 27 | const response = await server.inject(options) 28 | Code.expect(response.statusCode).to.equal(400); 29 | Code.expect(response.result.message).to.equal('Invalid request params input'); 30 | }); 31 | }); 32 | 33 | lab.experiment("Authentication Required to View Photo", function() { 34 | // tests 35 | lab.test("Deny view of photo if unauthenticated /photo/{id*} ", async function() { 36 | const options = { 37 | method: "GET", 38 | url: "/photo/8795" 39 | }; 40 | // server.inject lets you similate an http request 41 | const response = await server.inject(options) 42 | Code.expect(response.statusCode).to.equal(401); // Expect http response status code to be 401 ("Unauthorized") 43 | Code.expect(response.result.message).to.equal("Please log-in to see that"); // (Don't hard-code error messages) 44 | }); 45 | }); 46 | --------------------------------------------------------------------------------