Categories
Hapi

Server-Side Development with Hapi.js — Routing

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

Routing

We can add routes easily to our Hapi app.

For example, we can write:

const Hapi = require('@hapi/hapi');

const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0',
  });

  server.route({
    method: 'GET',
    path: '/',
    handler (request, h) {
      return 'Hello World!';
    }
  });

  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});

init();

to add a simple route with:

server.route({
  method: 'GET',
  path: '/',
  handler(request, h) {
    return 'Hello World!';
  }
});

The method has the request method the route accepts.

path has the route path.

handler has the route handler to handle the request.

request has the request data.

One route can handle multiple request methods.

For example, we can write:

const Hapi = require('@hapi/hapi');

const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0',
  });

  server.route({
    method: ['PUT', 'POST'],
    path: '/',
    handler (request, h) {
      return 'Hello World!';
    }
  });

await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});

init();

We have a route that handles both 'PUT' and 'POST' requests.

Path

A route can accept URL parameters if we put a parameter placeholder in our route path.

For example, we can write:

const Hapi = require('@hapi/hapi');

const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0',
  });

  server.route({
    method: 'GET',
    path: '/hello/{user}',
    handler: function (request, h) {
      return `Hello ${request.params.user}!`;
    }
  });

  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});

init();

We have a route that takes the user request parameter.

Then we can get the route parameter value from the request.params.user property.

Now if we make a GET request to the /hello/bob route, we get Hello bob! .

The parameter value isn’t escaped by default. We can escape the string that’s passed by writing:

const Hapi = require('@hapi/hapi');
const Hoek = require('@hapi/hoek');

const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0',
  });

  server.route({
    method: 'GET',
    path: '/hello/{user}',
    handler: function (request, h) {
      return `Hello ${Hoek.escapeHtml(request.params.user)}!`;
    }
  });

  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});

init();

We required the @hapi/hoek package and then call Hapi.escapeHtml on the request.params.user string to escape it.

This will prevent any cross-site scripting attacks since any code strings will be sanitized.

Optional Parameters

We can make a parameter optional with the ? .

For instance, we can write:

const Hapi = require('@hapi/hapi');
const Hoek = require('@hapi/hoek');

const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0',
  });

  server.route({
    method: 'GET',
    path: '/hello/{user?}',
    handler: function (request, h) {
      const user = request.params.user || 'anonymous'
      return `Hello ${user}!`;
    }
  });

  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});

init();

We have {user?} in the route string to make the user parameter optional.

Then when we go to /hello , we get Hello anonymous!

And when we go to /hello/bob , we get Hello bob! .

Conclusion

We can add various kinds of routes with Hapi.

Categories
Hapi

Server-Side Development with Hapi.js — Plugins

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

Create a Plugin

We can create plugins and use them with Hapi.

For example, we can write:

index.js

const Hapi = require('@hapi/hapi');

const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0',
  });

  await server.register(require('./myPlugin'));

  server.route({
    path: '/',
    method: 'GET',
    handler(request, h) {
      return h.response('hello')
    },
  });
  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});

init();

myPlugin.js

const myPlugin = {
  name: 'myPlugin',
  version: '1.0.0',
  register: async function (server, options) {
    server.route({
        method: 'GET',
        path: '/test',
        handler: function (request, h) {
          return 'hello, world';
        }
    });
  }
};

module.exports = myPlugin

We have the myPlugin object with the register method to add the routes we want in the plugin.

We specify the name and version of the plugin to set the metadata.

Then in index.js , we have:

await server.register(require('./myPlugin'));

to register the plugin we added.

We can pass in some options by writing:

index.js

const Hapi = require('@hapi/hapi');

const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0',
  });

  await server.register({
    plugin: require('./myPlugin'),
    options: {
      name: 'Bob'
    }
  });

  server.route({
    path: '/',
    method: 'GET',
    handler(request, h) {
      return h.response('hello')
    },
  });
  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});

init();

myPlugin.js

const myPlugin = {
  name: 'myPlugin',
  version: '1.0.0',
  register: async function (server, options) {
    server.route({
      method: 'GET',
      path: '/test',
      handler: function (request, h) {
        return options.name;
      }
    });
  }
};

module.exports = myPlugin

We get the option.name to get the name option in myPlugin.js .

Then to register the plugin, we write:

await server.register({
  plugin: require('./myPlugin'),
  options: {
    name: 'Bob'
  }
});

to register it with the plugins property to require the plugin file.

options lets us pass in the options.

We can also set a route prefix for the plugin.

For instance, we can write:

index.js

const Hapi = require('@hapi/hapi');

const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0',
  });

  await server.register(require('./myPlugin'),{
    routes: {
      prefix: '/plugins'
    }
  });

  server.route({
    path: '/',
    method: 'GET',
    handler(request, h) {
      return h.response('hello')
    },
  });
  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});

init();

myPlugin.js

const myPlugin = {
  name: 'myPlugin',
  version: '1.0.0',
  register: async function (server, options) {
    server.route({
      method: 'GET',
      path: '/test',
      handler: function (request, h) {
        return 'hello world'
      }
    });
  }
};

module.exports = myPlugin

We create the plugin the same way as in the first plugin example.

Then in index.js , we write:

await server.register(require('./myPlugin'), {
  routes: {
    prefix: '/plugins'
  }
});

to set the route prefix.

Now when we go to the /plugins/test route, we see hello world as the response.

Conclusion

We can create plugins to modularize our Hapi app code.

Categories
Hapi

Server-Side Development with Hapi.js — Cookies and Logs

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

Getting Cookies

We can get cookies with the reques.state property.

For instance, we can write:

const Hapi = require('@hapi/hapi');
const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0'
  });
  server.route({
    path: '/',
    method: 'GET',
    handler(request, h) {
      const value = request.state.data;
      return h.response(value);
    },
  });
  await server.start();
  console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We get the data cookie with the request.state.data property.

When we send the Cookie request header with data=foo as the value, then we see foo in the response.

Clearing a Cookie

We can clear a cookie by calling the unstate method.

For example, we can write:

const Hapi = require('@hapi/hapi');
const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0'
  });
  server.route({
    path: '/',
    method: 'GET',
    handler(request, h) {
      return h.response('hello').unstate('data');
    },
  });
  await server.start();
  console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call unstate with the 'data' key to clear the cookie with the key data .

Logging

We can use the server.log or request.log method to log data.

For example, we can write:

const Hapi = require('@hapi/hapi');
const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0'
  });
  server.route({
    path: '/',
    method: 'GET',
    handler(request, h) {
      request.log('error', 'something wrong');
      return h.response('hello')
    },
  });
  await server.start();
  console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

to call request.log with the tag to log and the log content respectively.

The first argument can be a string or an array of strings.

The 2nd argument is optional.

If we want to log something outside a request handler, we can use the server.log method.

For example, we can write:

const Hapi = require('@hapi/hapi');
const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.log(['test', 'error'], 'Test event');

  server.route({
    path: '/',
    method: 'GET',
    handler(request, h) {
      return h.response('hello')
    },
  });
  await server.start();
  console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

Retrieving and Displaying Logs

To get the log data and display it, we listen to the log event.

For example, we can write:

const Hapi = require('@hapi/hapi');
const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0',
    debug: { request: ['error'] }
  });

  server.route({
    path: '/',
    method: 'GET',
    options: {
      log: {
        collect: true
      }
    },
    handler(request, h) {
      request.log('error', 'Event error');
      return h.response('hello')
    },
  });
  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We add the debug property to the object in Hapi.server to display the logs.

Then we add options.log.collect property in the route handler object and set that to true .

And then we call request.log to log the event in the request handler.

Then we get the event logged.

Conclusion

We can get and clear cookies and log data with Hapi.

Categories
Hapi

Server-Side Development with Hapi.js — Caching and Cookie

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

Caching

We can control caching by setting some properties in our routes.

For example, we can write:

const Hapi = require('@hapi/hapi');

const init = async () => {

  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    path: '/{ttl?}',
    method: 'GET',
    handler(request, h) {
      const response = h.response({
        hello: 'world'
      });
      if (request.params.ttl) {
        response.ttl(request.params.ttl);
      }
      return response;
    },
    options: {
      cache: {
        expiresIn: 30 * 1000,
        privacy: 'private'
      }
    }
  });

  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});

init();

We call the route with server.route .

Then we call response.ttl to set the duration of the caching of the response.

We also set the options.cache.expiresIn property to set when the cache expires.

And privacy set whether the cache is private.

Last-Modified Header

We can also set the Last-Modified header added automatically to the response.

For instance, we can write:

const Hapi = require('@hapi/hapi');
const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0'
  });
  server.route({
    path: '/{ttl?}',
    method: 'GET',
    handler(request, h) {
      return h.response({ hello: 'world' })
        .header('Last-Modified', lastModified.toUTCString());
    },
    options: {
      cache: {
        expiresIn: 30 * 1000,
        privacy: 'private'
      }
    }
  });
  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call header with the Last-Modified key and the and the value

Cookies

We can add a cookie by calling the h.state method.

For instance, we can write:

const Hapi = require('@hapi/hapi');
const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    path: '/',
    method: 'GET',
    async handler (request, h) {
      h.state('data', { firstVisit: false });
      return h.response('Hello');
    },
  });
  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call h.state with the key and value.

Also, we can write:

const Hapi = require('@hapi/hapi');
const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    path: '/',
    method: 'GET',
    async handler (request, h) {
      return h.response('Hello').state('data', { firstVisit: false });
    },
  });
  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

to do the same thing.

We can override the options by writing:

const Hapi = require('@hapi/hapi');
const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    path: '/',
    method: 'GET',
    async handler (request, h) {
      return h.response('Hello').state('data', 'test', { encoding: 'none' });
    },
  });
  await server.start();
  console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We set the encoding option by calling the state method with a 3rd argument.

Conclusion

We can add caching and set cookies in our web app with Hapi.

Categories
Hapi

Server-Side Development with Hapi.js — Login and Cookie Auth

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

Cookie

We can add cookie authentication easily with a few lines of code.

For example, we can write:

const Bcrypt = require('bcrypt');
const Hapi = require('@hapi/hapi');

const users = [{
  username: 'john',
  password: '$2a$10$iqJSHD.BGr0E2IxQwYgJmeP3NvhPrXAeLSaGCj6IR/XU5QtjVu5Tm',
  name: 'John Doe',
  id: '1'
}];

const start = async () => {

  const server = Hapi.server({
    port: 4000,
    host: '0.0.0.0'
  });

  await server.register(require('@hapi/cookie'));

  server.auth.strategy('session', 'cookie', {
    cookie: {
      name: 'sid-example',
      password: '!wsYhFA*C2U6nz=Bu^%A@^F#SF3&kSR6',
      isSecure: false
    },
    redirectTo: '/login',
    validateFunc: async (request, session) => {
      const account = await users.find(
        (user) => (user.id === session.id)
      );

      if (!account) {
        return {
          valid: false
        };
      }

      return {
        valid: true,
        credentials: account
      };
    }
  });

  server.auth.default('session');

  server.route([{
      method: 'GET',
      path: '/',
      handler (request, h) {
        return 'Welcome to the restricted home page!';
      }
    },
    {
      method: 'GET',
      path: '/login',
      handler (request, h) {
        return `<html>
            <head>
                <title>Login page</title>
            </head>
            <body>
                <h3>Please Log In</h3>
                <form method="post" action="/login">
                    Username: <input type="text" name="username"><br>
                    Password: <input type="password" name="password"><br>
                <input type="submit" value="Login"></form>
            </body>
        </html>`;
      },
      options: {
        auth: false
      }
    },
    {
      method: 'POST',
      path: '/login',
      handler: async (request, h) => {

        const {
          username,
          password
        } = request.payload;
        const account = users.find(
          (user) => user.username === username
        );

        if (!account || !(await Bcrypt.compare(password, account.password))) {
          return h.view('/login');
        }

        request.cookieAuth.set({
          id: account.id
        });

        return h.redirect('/');
      },
      options: {
        auth: {
          mode: 'try'
        }
      }
    }
  ]);

  await server.start();
  console.log('server running at: ' + server.info.uri);
};

start();

We have the users array with the user data.

password is the hash of the password. It’s 'secret' in plain text.

The start function has the code for our app.

The Hapi.server function creates the server.

We register the @hapi/cookie module with:

await server.register(require('@hapi/cookie'));

Then we add the user authentication fucntion with:

server.auth.strategy('session', 'cookie', {
  cookie: {
    name: 'sid-example',
    password: '!wsYhFA*C2U6nz=Bu^%A@^F#SF3&kSR6',
    isSecure: false
  },
  redirectTo: '/login',
  validateFunc: async (request, session) => {
    const account = await users.find(
      (user) => (user.id === session.id)
    );

    if (!account) {
      return {
        valid: false
      };
    }

    return {
      valid: true,
      credentials: account
    };
  }
});

We call the server.auth.strategy method to add authentication.

'session' is the name. 'cookie' is the strategy.

The object has the validation logic. cookie sets the cookie secret.

redirect adds the redirect when auth fails.

validateFunc has the logic to validate the credentials.

We check if the user has a valid cookie with:

const account = await users.find(
  (user) => (user.id === session.id)
);

We return an object with the valid property to indicate whether the credentials are valid.

credentials has the credentials for the account.

Then we call server.route to add the routes. The GET / route is the restricted route.

GET /login displays the login form.

We set options.auth to false to let us access the page without authentication.

The POST /login route lets us search for the user with the given username and password.

We validate the password with Bcrypt.compare .

And then we call h.redirect to the route we want to access if the username and password are valid.

Otherwise, we redirect to the login route.

Conclusion

We can create an app with a login page and cookie authentication easily with Hapi.