1 of 88

Build an API with Hapi.js

2 of 88

Hello! I’m Ryan

  • Product Owner at Auth0
  • Google Developer Expert
  • Angularcasts.io

2

@ryanchenkie

chenkie

3 of 88

WHAT WE’LL BUILD

3

4 of 88

We’ve got an Angular app to manage FEM instructors

Now we need an API

4

LET’S MAKE AN API

  • Get all instructors
  • Get one instructor
  • Add a new instructor

5 of 88

5

6 of 88

6

7 of 88

What we’ll need

  1. Node.js and npm
  2. Postman
  3. The Angular app

7

8 of 88

  • Clone the repo bit.ly/fem-frontend
  • Install the Angular CLI npm install @angular/cli
  • Install the dependencies npm install
  • Run the app npm start

8

GET THE ANGULAR APP

9 of 88

  • Clone the repo bit.ly/fem-backend
  • Install the dependencies npm install
  • Switch between branches git checkout branch-name
  • Run the API npm start

9

GET THE HAPI API

10 of 88

1.

What is Hapi.js All About?

10

11 of 88

Hapi.js

“A rich framework for building applications and services”

11

12 of 88

Hapi.js History

What is Hapi.js used for?

  • Originally created by Walmart Labs
  • Lead maintainer: Eran Hammer
  • Data APIs
  • Services
  • Regular websites

12

13 of 88

Not the most popular kid

13

HOW DOES HAPI STACK UP?

14 of 88

  • Batteries included

Error objects, validation, sessions, CORS, logging

  • Ecosystem
  • Simplicity & Organization

14

WHY HAPI?

15 of 88

The Hapi.js Ecosystem

Error Objects

Validation

HTTP Client

Utilities

Process Monitoring

15

16 of 88

What does a Hapi.js app look like?

16

17 of 88

Wiring up a server is dead simple

17

CREATE A SERVER INSTANCE

const Hapi = require('hapi');

const server = new Hapi.Server();

18 of 88

To make it do something, define a route

18

DEFINE A ROUTE

server.route({� method: 'GET',

path: '/api/stuff',

handler: (request, reply) => { ... }

});

19 of 88

Tell Hapi which port to listen on and start the server

19

START THE SERVER

server.connection({ port: 3001 });

server.start(err => {

if (err) throw err;

});

20 of 88

Send a response to the client using reply

20

THE REPLY INTERFACE

...

handler: (request, reply) => {

reply('Hey there');

}

21 of 88

Challenge #1

  • git checkout module-1-start
  • Create a Hapi.js server that listens on port 3001
  • Create a single route which responds to a GET request
  • When the server starts, use the server.info object to log the URI out to the console

WIRE UP A SIMPLE APP

21

22 of 88

2.

Routes and Route Handlers

22

23 of 88

When it comes down to it, the web is all about request and response

23

24 of 88

REST API Principles Review

  • REST describes how to make resources available in a client-server relationship
  • Data should be organized around resources
  • API should respond to common HTTP verbs

GET, POST, PUT, PATCH, DELETE

  • Server should be stateless

A FEW CORE PRINCIPLES

24

25 of 88

REST API Principles Review

  • Get all contacts
  • Get one contact
  • Create a contact
  • Edit a contact
  • Delete a contact

OPERATIONS ON A SINGLE RESOURCE

Given some contacts resource, we might want multiple endpoints to have full CRUD operations

25

GET /contacts

GET /contacts/42

POST /contacts

PUT /contacts/42

DELETE /contacts/42

26 of 88

At a minimum, a route needs three things:�method, path, handler

Routes are configured with an options object

26

ROUTE CONFIGURATION

27 of 88

27

�� server.route({� method: 'GET',

path: '/api/stuff',

handler: (request, reply) => { ... }

});

28 of 88

When we send a request to an endpoint, we want some action to take place

The route handler is where the magic happens

Example: save to a database, calculate a value, fetch an external resource

28

THE ROUTE HANDLER

29 of 88

For the route handler to do its job, it uses a request object and a reply interface

29

THE ROUTE HANDLER

30 of 88

The Request Object

  • Query string
  • Params string
  • Payload
  • Headers
  • Auth
  • Route prerequisite

For each incoming request, a request object is created

The request object has useful information about the request

30

31 of 88

GET /api/contacts/42?profile=true

31

DATA ON THE REQUEST OBJECT

path: '/api/contacts/{id}'

handler: (request, reply) => {

const id = request.params.id; // 42

const profile = request.query.profile // true

}

32 of 88

The Reply Interface

  • When used as a callback, returns control to Hapi
  • When used a response generator, sends a response to the client

A function argument to respond to requests

Used as a callback interface and a response generator depending on where it is used

32

33 of 88

Send a response to the client using reply

33

THE REPLY INTERFACE

...

handler: (request, reply) => {

reply('Hey there');

}

34 of 88

Challenge #2

  • git checkout module-2-start
  • When a GET request is made to /api/instructors, return all the instructors data
  • When a GET request is made to /api/instructors/{slug}, return the specific instructor
  • For the single instructor route, send back only the id, name, and slug

RETURN SOME DATA

34

35 of 88

Challenge #2

  • We’re storing the data in-memory
  • We can use simple JavaScript functions like map and find to fake out database queries
  • Make sure to check whether the resource exists before sending anything back

NOTES

35

36 of 88

Challenge #2

  • Provide a way to sort the data based on a sortKey and sortDirection
  • Use the query string to specify these options
  • Hint: use Lodash to do the sorting

BONUS

36

37 of 88

3.

Routes Prerequisites

37

38 of 88

APIs don’t operate in a vacuum

38

39 of 88

  • Pull data out of a database and save new data to it
  • Call other APIs and services
  • Run complex, resource intensive tasks (calculations)

Regardless of the task, it needs to complete before a reply can be sent to the client

39

COMMON API TASKS

40 of 88

Typical Approach:�Everything in the Handler

40

41 of 88

41

path: '/api/contacts/{id}',

handler: (request, reply) => {

getContactData({ id },(err, data) => {

getContactPhoto({ id }, (err, data) => {

getOtherInfo((err, data) => {

// on and on

});

});

});

}

42 of 88

42

path: '/api/contacts/{id}',

handler: (request, reply) => {

getContactData({ id }).then(data => {

return getContactPhoto({ id });

}).then(data => {

return getOtherInfo();

}).then(data => {

// other tasks

}).catch(err => ...);

}

43 of 88

  • Prerequisite functions are run before anything else
  • Values can be picked up in the handler
  • Async operations complete before the handler is reached

A better way: specify some action to happen before the

route handler is reached

43

ROUTE PREREQUISITES

44 of 88

  • Array of objects on config.pre
  • At a minimum, specify a method

What does a route prerequisite look like?

44

ROUTE PREREQUISITES

45 of 88

45

config: {

pre: [

{ method: query.verifyUniqueEmail },

{ method: query.createContactSlug, assign: 'slug' }

],

handler: (request, reply) => {

reply(request.pre.slug);

}

}

46 of 88

46

const verifyUniqueEmail = (request, reply) => {

const email = request.payload.email;

if (!emailExists(email)) {

reply();

}

};

47 of 88

47

const createContactSlug = (request, reply) => {

const name = request.payload.name;

const slug = name.split(' ').join('-');

reply(slug.toLowerCase());

};

48 of 88

Route Prerequisites

Methods need to have the same request and reply signature found in handlers

Same access to request information

Calling reply passes control back to the framework

Assigning a value will put it on request.pre in the handler (optional)

48

49 of 88

Route Prerequisites

By default, methods will be called in succession

Putting methods in another array will run them in parallel

BENEFITS

  • Small, reusable units
  • Better organization
  • Don’t need to worry about handling async

49

50 of 88

Challenge #3

  • git checkout module-3-start
  • When a POST request is made to /api/instructors, add a new instructor to the array
  • Create a route prerequisite which checks whether the instructor already exists
  • Create a route prerequisite which creates a slug for the instructor and assigns it to a key for use in the handler

POST SOME DATA

50

51 of 88

Challenge #3

  • Github API requires a user agent
  • { “User-Agent” : “whatever” }

NOTES

51

52 of 88

Challenge #3

  • Use the Wreck library to make a call to the Github API to get the instructor’s avatar URL
    • Demonstrates that route prerequisites work asynchronously
  • This should be done in the GET /instructors/{slug} route

BONUS

52

53 of 88

Challenge #3

  • Instructor ID can be incremented based on the length of the array
  • When an instructor is added, reply with the newly created object

NOTES

53

54 of 88

4.

Smart Error Objects with Boom

54

55 of 88

When something goes wrong in the request, we need to handle the error

55

ERROR HANDLING

56 of 88

  • Resource doesn’t exist
  • User isn’t authorized to access the resource
  • Client is making too many requests

What could go wrong?

56

ERROR HANDLING

57 of 88

Handling Errors

We need to respond with an error code, usually something in the 400s

Provide a message to the user about what went wrong

Doing this manually is a pain

  • How should the returned object be shaped?
  • What if we want to return data or specify headers?
  • What’s that error code again?

57

58 of 88

Boom simplifies error responses

58

59 of 88

With Boom, just provide a message

59

ERRORS WITH BOOM

const Boom = require('boom');

handler: (request, reply) => {

if (!contacts) {

reply(Boom.notFound('No contacts found!'));

}

}

60 of 88

Call any HTTP error reason

60

ERRORS WITH BOOM

reply(Boom.<error_reason>(message));

61 of 88

Handling Errors with Boom

  • Error objects are easy to create
  • Consistency
  • Alignment with Hapi and ecosystem libraries which use Boom internally

BENEFITS

61

62 of 88

Challenge #4

  • In the resources we’ve created, use Boom to return an error if something goes wrong
    • No instructor(s) found
    • Instructor already exists

RETURN ERRORS WITH BOOM

62

63 of 88

5.

Validation with Joi

63

64 of 88

  • Data validity
  • Data consistency
  • Security

When it comes to payloads, queries, and parameters, we shouldn’t accept just anything

64

ROUTE VALIDATION

65 of 88

65

<input type="text" placeholder="Name">

<input type="email" placeholder="Email">

66 of 88

Validating Route Data

Validating incoming payloads, params, and queries by hand would be cumbersome

At the end of the day, we’d be writing a library to do this

  • What should the schema validation look like?
  • How do we respond when something fails?
  • Do we have all edge cases covered?

66

67 of 88

Joi simplifies object schema validation

67

68 of 88

With Joi, describe what the payload, params, or query should look like

68

VALIDATION WITH JOI

const Joi = require('joi');

const paramsValidator = Joi.object({

slug: Joi.string()

});

69 of 88

Be as specific or generic as you want

69

VALIDATION WITH JOI

const payloadValidator = Joi.object({

name: Joi.string().required(),

skills: Joi.string().valid(['javascript', 'node'])

});

70 of 88

Use built-in utilities to ease validation

70

VALIDATION WITH JOI

const payloadValidator = Joi.object({

email: Joi.string().email().required(),

title: Joi.string().regex(/[A-Za-z0-9]/)

});

71 of 88

How do we use it?

71

VALIDATION WITH JOI

config: {

validate: {

payload: payloadValidator

}

}

72 of 88

Object Schema Validation with Joi

What can be validated? Lot’s of things.

  • Strings
  • Arrays
  • Numbers
  • Objects
  • Date
  • Boolean

72

73 of 88

Object Schema Validation with Joi

Common use cases

  • Required
  • Min/Max
  • Email
  • Credit Card
  • Regex
  • Valid (whitelisting)

73

74 of 88

What happens when validation fails?

74

VALIDATION WITH JOI

error: "Bad Request"

message: "child "name" fails because ["name" is not allowed to be empty]"

statusCode: 400

75 of 88

Challenge #5

  • git checkout module-5-start
  • Query for GET /api/instructor
  • Payload for POST /api/instructor
  • Params for GET /api/instructor/{slug}

ADD VALIDATION FOR OUR ROUTES

75

76 of 88

Challenge #5

  • We know what the only acceptable values for sortDirection and sortKey are
  • For the POST route, name and email are required

NOTES

76

77 of 88

Challenge #5

  • Twitter handles require an @ symbol and are limited to 15 characters - provide a regex to check for this

BONUS

77

78 of 88

6.

Plugins

78

79 of 88

Plugin Ecosytem

Libraries like Boom and Joi are part of the “extended Hapi universe”

Many other (often smaller) libraries outside of the Hapi lineup are provided by the community

Plugins serve a number of needs

  • Authentication
  • Localization
  • Documentation
  • Templating
  • Utility

79

80 of 88

After installation, plugins need to be registered

80

USING PLUGINS

const Plugin = require('plugin');

server.register(Plugin, () => {});

81 of 88

Most often, we’ll want to register some options

81

USING PLUGINS

server.register({

register: Plugin,

options: {

someOption: true

}

}, err => ... );

82 of 88

Challenge #6

  • To get a feel for Hapi plugins, install Inert and hapi-geo-locate
  • Inert is used for serving static files. Make the GET /api/images/{name}route respond with the image in the public directory
  • hapi-geo-locate is used to get the user’s IP address. Simply log out the IP address when a request is made for an image

ADD SOME PLUGINS

82

83 of 88

Challenge #6

  • Check the readmes for these two plugins to learn about how to configure them

NOTES

83

84 of 88

WRAPPING UP

84

85 of 88

Hapi.js Offers a Lot

  • Does a lot out-of-the-box
  • Rich ecosystem of libraries and plugins
  • Great approach to organization
  • Modularity
  • Enjoyable to build with

85

86 of 88

Where to go from here

  • Explore the Hapi ecosystem
  • Server methods
  • Authentication
  • Go and build

86

87 of 88

Drop me a line

87

@ryanchenkie

chenkie

88 of 88

THANK YOU!

88