Why Hapi

You’ve probably used node.js WEB frameworks like Express, Koa2, etc. When building WEB applications, your job is simply to produce RESTFUL apis or call other network interfaces from Node. You might be wondering if there’s an easier way to handle requests, or if there’s a Node framework that doesn’t have to agonize over which middleware to use early in a building project. After comparing multiple frameworks, I chose to use Hapi to refactor my Koa2 project.

Hapi is currently Github Star 10653, latest version 17.5, release 18.x. Six, yes, you read that right, single digit. You can see that the attention and maintenance status of Hapi is very good. Check out Hapi’s website for updates on Hapi, including submission, issues changed, a brief tutorial on features, API documentation with examples, the Hapi community, plug-ins, and resources. Hapi has a complete set of plugins to build WEB applications, some official, some community contributions, and often these plugins can be used anywhere you want without relying on Hapi, such as Boom, Joi, Catbox.

If you want to learn more about Hapi, or how it differs from other frameworks, you can do a Google search. This article won’t cover much about the framework.

node-frameworks-to-use

Framework of contrast

Hapijs

For what kind of readers

You don’t need to have any experience with Node to follow this tutorial. You can use it as an introduction to Node. If you’re a front-end developer, this tutorial will give you a better idea of what Node can do and how the front and back ends deliver their work. You may also be new to other Node frameworks, so you can use this introductory tutorial to compare and contrast the two frameworks. If you are already an experienced Node developer, this tutorial is not for you.

This tutorial covers fewer concepts and is more hands-on, so you can start learning even if you don’t have any experience.

To prepare

  • Install the node
  • Create a project
  • Initialization package. Json
  • The editor recommends vscode
  • Command-line tools – Windows recommends cmder, Mac recommends iTerm2
npm init -y
// or
npm init
// The -y argument initializes package.json by default
Copy the code
  • Installing Hapi
npm i hapi
// or
npm install hapi -D
// I is short for install. If no parameter is set, the default value is -d
Copy the code

A service

// server.js
const Hapi = require('hapi')

const server = Hapi.server({
    port: 3000.host: 'localhost'
})

const init = async() = > {await server.start()
    console.log(`Server running at: ${server.info.uri}`)
}
init()
Copy the code

Execute on the command line

node server.js
# Server running at: http://localhost:3000
# indicates that our service has been started
If port 3000 is already used, you can change port to another port
Copy the code

Now we go to http://localhost:3000 and the page shows 404 because we haven’t configured any routes.

1. The routing

// server.js

const init = async () => {
    server.route({
        path: '/',
        method: 'GET',
        handler () {
            return 'Hapi world'
        }
    })
    await server.start()
    console.log(`Server running at: ${server.info.uri}`)
}

Copy the code

Now restart the service and we can see what is on the page.

Next we create an API that returns a JSON data

// server.js
server.route({
    path: '/api/welcome'.method: 'GET',
    handler () {
        return {
            code: 200.success: true.data: {
                msg: 'welcome'}}}})Copy the code

Restart the service, we visit http://localhost:3000/api/welcome

We get a data with a content-type of Application/JSON. We can request this interface through XMLHttpRequest libraries such as jQuery Ajax, Axios, Fetch, and get a JSON data

2. Stop

Wait a minute. Did you realize that every time we change a file, we have to disconnect the service and manually restart it? That’s too bad.

npm i onchange
# add onchange module
Copy the code
// package.json
"scripts": {
    "dev": "node server.js"."watch": "onchange -i -k '**/*.js' -- npm run dev"
},
Copy the code

We added a dev execution to the scripts field of the package.json file. In this way, we execute NPM run dev, which is equivalent to executing node server.js. Using the onchange package, monitor my JS file changes and restart the service when the file changes.

Have a try

npm run watch
Copy the code

Then let’s modify the API /welcome return

Refresh your browser

Look! There is no need to restart the service manually. Every time you make a change, you just need to refresh the browser to see the result

Now we don’t need to introduce Nodemon too early, although it is very good and useful.

3. The parameters

Now that we can request data from the server, we also need to pass data from the client to the server, and we’ll look at several ways to pass parameters.

Let’s assume a couple of scenarios to understand how to get parameters.

  1. /api/welcomeWe want it to return the name that was passed in
Server. route({path: '/ API /welcome', method: 'GET', handler (request) {return {code: 200, success: True, data: {MSG: ` welcome ${request. Query. The name} `}}}}) / / request/http://localhost:3000/api/welcome?name=kenny/MSG: "welcome kenny"Copy the code
  1. The name parameter is redundant because this interface accepts only one parameter, so the name is now omitted
Server. route({path: '/ API /welcome/{name}', method: 'GET', handler (request) {return {code: 200, success: true, data: { msg: `welcome ${request.params.name}` } } } }) // http://localhost:3000/api/welcome/kenny // msg: "Welcome Kenny" // The result is the sameCopy the code
  1. Suppose we need to change our welcome words occasionally, but not every time to change the code, then we need an interface to replace the welcome words, by submitting the interface to replace the welcome words.
let speech = {
    value: 'welcome',
    set (val) {
        this.value = val
    }
}
server.route({
    path: '/api/welcome/{name}'.method: 'GET',
    handler (request) {
        return {
            code: 200.success: true.data: {
                msg: `${speech.value} ${request.params.name}`
            }
        }
    }
})
server.route({
    path: '/api/speech'.method: 'POST',
    handler (request) {
        speech.set(request.payload.word)
        return {
            code: 200.success: true.data: {
                msg: `speech is *${speech.value}* now`}}}})Copy the code

verify

Using curl to verify a POST interface, you can also use Ajax, POSTMAN... Wait any way you like.Curl, the form of word = hello \ http://localhost:3000/api/speech# {" code ": 200," success ": true," data ": {" MSG" : "researched is * hello * now"}} %
curl http://localhost:3000/api/welcome/kenny
# {" code ": 200," success ": true," data ": {" MSG" : "hello Kenny"}} %
Copy the code

Note the difference between Content-Type Application/X-www-form-urlencoded and multipart/form-data.

You can use request.query to get data from the URL queryString, request.payload to get data from the REQUEST body of the POST interface. Request. params gets the custom parameters in the URL.

4. Second service

We already have a back-end API service, and there should be a front-end service, which may be single-page or traditional back-end rendering pages, but usually not on the same port as your back-end service. We created another service to render the front page in order to more realistically simulate the real scene.

+const client = Hapi.server({
+ port: 3002,
+ host: 'localhost'
+})
+

- server.route({
+ client.route({

+ await client.start()
Copy the code

Add a new service, listen port 3002, and change the previous home route to the client home.

Visit http://localhost:3002 to see the results

5. Static files

Previously, we rendered the page directly as a string, which was not easy to write and modify, so we changed the way we returned HTML to “template” rendering.

Install required dependencies
npm i inert
Create a public folder
mkdir public
# to create index. HTML
touch public/index.html
# to create the about HTML
touch public/about.html
Copy the code

      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    <title>Document</title>
</head>
<body>
    <h1>Hapi world</h1>
</body>
</html>
Copy the code
// ...

const client = Hapi.server({
    port: 3002,
    host: 'localhost',
    routes: {
        files: {
            relativeTo: Path.join(__dirname, 'public')
        }
    }
})

// ... 
// const init = async () => {
await client.register(Inert)
client.route({
    path: '/{param*}',
    method: 'GET',
    handler: {
        directory: {
            path: '.',
            index: true,
        }
    }
})

// ...
Copy the code

Visit in turn to see the effect

  • http://localhost:3002
  • http://localhost:3002/index.html
  • http://localhost:3002/about.html

The /index.html path with the extension looks a bit unprofessional, so let’s change the configuration of directory

directory: {
+ defaultExtension: 'html'
Copy the code

Go to http://localhost:3002/index

6. Cross-domain request

The same origin policy for XHRHttpRequest (port 3002) will cause CORS problems when sending XHRHttpRequest to server (port 3000). Cross-domain requests are allowed by setting response headers such as access-Control-allow-Origin.

// index.html
$.ajax({
    url: 'http://localhost:3000/api/welcome/kenny'
}).then(function (data) {
    console.log(data)
})
Copy the code

http://localhost:3002/index will offer the cross-domain js errors

Access to XMLHttpRequest at ‘http://localhost:3000/api/welcome/kenny’ from origin ‘http://localhost:3002’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

// server.js
const server = Hapi.server({
    port: 3000,
    host: 'localhost',
    routes: {
        cors: {
            origin: '*'
        }
    }
})
Copy the code

After saving, you will find the following error on the terminal

[1] “origin” must be an array

This is another advantage of Hapi, configuration checking, because as a configuration first framework, Hapi does a lot of configuration checking. When you use a configuration that is not allowed or not standard, there will be corresponding errors. It is convenient for you to catch and solve the problems.

origin: [The '*']
Copy the code

Then refresh the page and you will find that the cross-domain error is gone.

We haven’t said anything about cross-domains:

  • Allows the specified domain name and multiple domain names
  • Allow cookies [access-control-allow-credentials]
  • Allows Access to additional Headers [access-Control-expose-headers]
  • Allowed Headers [access-Control-allow-headers]

7. What else is missing?

Currently we have a front-end service for Web rendering, a back-end service that provides interfaces, and they are in different “domains” (ports), and the front-end pages may be written flat, without images and styles, and without Favicons.

  • Download a Favicon of your choice
  • Introduce a native CSS
  • Introduce a local image

Put them all in /public

.<head>.<link rel="icon" type="image/png" href="/favicon.png">
<link rel="stylesheet" href="/bulma.min.css">
</head>.<html>
<img class="logo" src="/logo.svg" />.Copy the code

8. Cookie

Assume that we have a login /login interface. After a successful login, set a login field in the cookie. The front end can use this login to judge whether you are logged in and can logout by /logout.

// ...
server.state('login', {
    ttl: null./ / time
    isSecure: false.// https
    isHttpOnly: false.// http Only
    encoding: 'none'.// encode
    clearInvalid: false.// Remove unavailable cookies
    strictHeader: true // Violations of RFC 6265 are not allowed
})
// ...
const init = async() = > {// ...
server.route({
    path: '/api/login'.method: 'POST',
    handler (request, h) {
        let body
        let code
        / / get a cookie
        const isLogin = request.state.login
        if (isLogin) {
            body = {
                msg: 'Logged in'
            }
            code = 200
        } else if (request.payload && request.payload.email === '[email protected]' && request.payload.password === '123456') {
            / / set the cookie
            h.state('login'.'true')
            body = {
                msg: 'Login successful'
            }
            code = 200
        } else {
            code = 100
            body = {
                msg: 'Incorrect login information'}}return {
            code,
            success: true.data: body
        }
    }
})
Copy the code
server.route({
    path: '/api/logout'.method: 'POST',
    handler (request, h) {
        / / cancel the cookie
        h.unstate('login')
        return {
            code: 200.success: true}}})Copy the code

This example is not suitable for a real business scenario, but simply describes how to set and cancel cookies

9. Authentication and authorization

The concept of authentication can be difficult to understand for beginners, such as the more common JWT (JSON Web Token). I won’t waste time explaining how to use it. If you want to know what JWT is, the portal: Learn how to use JSON Web Tokens (JWT) for Authentication. In the Hapi framework, we use hapi-Auth-jwT2

Here’s the convenience of authentication configuration in Hapi.

In Express/Koa2, you need

  • The introduction of the plugin
  • Middleware Processing 401
  • Middleware matches authentication routes and excludes unnecessary authentication routes.

As your project has enough routes, this matching rule becomes more and more complex. Or you can do a little planning in the naming of routes, which makes perfectionists feel bad. Making a judgment within a single route is repetitive.

Let’s look at the use of Hapi.

// Import plug-ins
await server.register(require('hapi-auth-jwt2'))
// Customize your authentication method
const validate = async function (decoded, request) {
    return {
        isValid: true}}// Set authentication
server.auth.strategy('jwt'.'jwt', {
    key: 'your secret key',
    validate,
    verifyOptions: {
        algorithms: ['HS256']},cookieKey: 'token'
})

// A route that requires authentication
server.route({
    path: '/user/info'.method: 'GET'.options: {
        auth: 'jwt'
    },
    // ...
})
// An optional route that requires authentication
server.route({
    path: '/list/recommond'.method: 'GET'.options: {
        auth: {
            strategy: 'jwt'.mode: 'optional'}},// ...
})
// A route that requires authentication attempts
server.route({
    path: '/list/recommond'.method: 'GET'.options: {
        auth: {
            strategy: 'jwt'.mode: 'try'}},// ...
})


Copy the code

The difference between an optional try and an optional try is that you don’t have to have an optional try, but it has to be correct. Try, it doesn’t matter, it doesn’t return 401.

It can be seen that authentication in Hapi is configured on the route, so that when managing authentication and non-authentication modules, you only need to configure the corresponding rules without worrying about whether the global configuration is incorrectly changed.

Log 10.

There is no place for us to look when a request is received or made on the service. Now join a logging system.

npm i hapi-pino
Copy the code
await server.register({
    plugin: require('hapi-pino'),
    options: {
        prettyPrint: true // Format output}})Copy the code

Re-service and access ‘/ API /logout’

Take a look at the terminal explicit

[1547736441445] INFO  (82164 on MacBook-Pro-3.local): server started
    created: 1547736441341
    started: 1547736441424
    host: "localhost"
    port: 3000
    protocol: "http"
    id: "MacBook-Pro-3.local:82164:jr0qbda5"
    uri: "http://localhost:3000"
    address: "127.0.0.1"
Server running at: http://localhost:3000

[1547736459475] INFO  (82164 on MacBook-Pro-3.local): request completed
    req: {
      "id": "1547736459459:MacBook-Pro-3.local:82164:jr0qbda5:10000"."method": "post"."url": "/api/logout"."headers": {
        "cache-control": "no-cache"."postman-token": "b4c72a2f-38ab-4c5c-9559-211e0669e6cf"."user-agent": "PostmanRuntime / 7.4.0"."accept": "* / *"."host": "localhost:3000"."accept-encoding": "gzip, deflate"."content-length": "0"."connection": "keep-alive"
      }
    }
    res: {
      "statusCode": 200,
      "headers": {
        "content-type": "application/json; charset=utf-8"."vary": "origin"."access-control-expose-headers": "WWW-Authenticate,Server-Authorization"."cache-control": "no-cache"."set-cookie": [
          "login=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Strict"]."content-length": 27
      }
    }
    responseTime: 16

Copy the code

Can say very comprehensive log, but also with coloring effect.

11. The document

As you add more and more interfaces to your project over time, a good document can make a lot of difference when working with other people or trying to find a definition for an interface

await server.register({
    plugin: require('lout')})Copy the code

Because Hapi is a configuration-centric framework, documentation can also be generated based on configuration. You only need to describe a route to generate a usable document.

Visit http://localhost:3000/docs to check the effect

12. Forwarding interface

unfinished

How to use examples

All the content mentioned in this article has been uploaded to Github

You can clone the project and then view the code. You can also switch to a different step (git checkout HEAD)


# see the commit
git log --pretty=online

51b2a7eea55817c1b667a34bd2f5c5777bde2601 part 9 api doc
fbb1a43f0f1bf4d1b461c4c59bd93b27aabc3749 Part8 cookies
00a4ca49f733894dafed4d02c5a7b937683ff98c Part7 static
ea2e28f2e3d5ef91baa73443edf1a01a383cc563 Part7 cors
a0caaedbf492f37a4650fdc33d456fa7c6ef46d3 Part6 html render
12fce15043795949e5a1d0d9ceacac8adf0079e8 Part5 client server
79c68c9c6eaa064a0f8c679ae30a8f851117d7e0 Part4 request.payload
e3339ff34d308fd185187a55f599feed1e46753e Part4 request.query
af40fc7ef236135e82128a3f00ec0c5e040d4b12 Part3 restart when file changed
2b4bd9bddfe565fd99c7749224e14cc7752525b1 Part2 route 2
99a8f8426f43fea85f98bc9a3b189e5e3386abfe Part2 route
047c805ca7fe44148bac85255282a4d581b5b8e1 Part1 server
# switch to Part5
git checkout 12fce15043795949e5a1d0d9ceacac8adf0079e8
Copy the code

At the end

At present, the completion degree of the tutorial is 80%. Due to the current limited energy, it will be updated here temporarily, and will be continuously updated to a satisfactory degree according to the comments and suggestions of readers.

Thanks again for reading, and if you found this tutorial helpful, please share the comments. Of course, you can also tip them.

If you have better suggestions for this tutorial, please contact me.