Categories
Hapi

Server-Side Development with Hapi.js — Rendering Templates

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.

Rendering Templates

We can render templates with the @hapi/vision module.

For instance, we can write:

index.js

const Ejs = require('ejs');
const Hapi = require('@hapi/hapi');
const Vision = require('@hapi/vision');

const server = Hapi.Server({ port: 3000 });

const rootHandler = (request, h) => {
  return h.view('index', {
    title: request.server.version,
    message: 'Hello Ejs!'
  });
};

const init = async () => {
  await server.register(Vision);

  server.views({
    engines: { ejs: Ejs },
    relativeTo: __dirname,
    path: 'templates'
  });

  server.route({ method: 'GET', path: '/', handler: rootHandler });

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

init();

templates/index.ejs

<%= message %>

We have await server.register(Vision); to register the @hapi/vision plugin.

Then we call h.view to render the view.

title and message will be available in the template.

We set the engines.ejs property to the Ejs module to use it as our rendering engine.

We can use the Handlebars view engine by writing:

index.js

const Handlebars = require('handlebars');
const Hapi = require('@hapi/hapi');
const Vision = require('@hapi/vision');

const server = Hapi.Server({ port: 3000 });

const rootHandler = (request, h) => {
  return h.view('index', {
    title: request.server.version,
    message: 'Hello!'
  });
};

const init = async () => {
  await server.register(Vision);

  server.views({
    engines: { html: Handlebars },
    relativeTo: __dirname,
    path: 'templates'
  });

  server.route({ method: 'GET', path: '/', handler: rootHandler });

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

init();

templates/index.html

{{message}}

We change the engines property to { html: Handlebars } and require the handlebars module to use the Handlebars views engine.

To use the Pug view engine with Hapi, we write:

index.js

const Pug = require('pug');
const Hapi = require('@hapi/hapi');
const Vision = require('@hapi/vision');

const server = Hapi.Server({ port: 3000 });

const rootHandler = (request, h) => {
  return h.view('index', {
    title: request.server.version,
    message: 'Hello!'
  });
};

const init = async () => {
  await server.register(Vision);

  server.views({
    engines: { pug: Pug },
    relativeTo: __dirname,
    path: 'templates'
  });

  server.route({ method: 'GET', path: '/', handler: rootHandler });

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

init();

templates/index.pug

p #{message}

To use the Mustache view engine, we write:

const Mustache = require('mustache');
const Hapi = require('@hapi/hapi');
const Vision = require('@hapi/vision');

const server = Hapi.Server({ port: 3000 });

const rootHandler = (request, h) => {
  return h.view('index', {
    title: request.server.version,
    message: 'Hello!'
  });
};

const init = async () => {
  await server.register(Vision);

  server.views({
    engines: {
      html: {
        compile: (template) => {
          Mustache.parse(template);
          return (context) => {
            return Mustache.render(template, context);
          };
        }
      }
    },
    relativeTo: __dirname,
    path: 'templates'
  });

  server.route({ method: 'GET', path: '/', handler: rootHandler });

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

init();

templates/index.html :

{{message}}

We have the engines.html.compile method to compile the template into rendered HTML.

context has the data we pass into the 2nd argument of h.view .

Conclusion

We can render various kinds of templates into HTML with the @hapi/vision plugin.

Categories
Hapi

Server-Side Development with Hapi.js — User-Agent, Payload, Semantic Version, and Sorting

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.

Get User Agent Info

We can get user agent info from the request with the @hapi/scooter module.

To use it, we write:

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

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

  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        return request.plugins.scooter;
      },
    }
  });
  await server.register(Scooter);
  await server.start();
  console.log('Server running at:', server.info.uri);
};

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

init();

We have await server.register(Scooter); to register the Scooter plugin.

And we use request.plugins.scooter to return the user agent object.

Semantic Versioning

We can use the @hapi/somever module to work with semantic version numbers.

For example, we can write:

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

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

  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        return somever.compare('1.2.3', '1.2.5');
      },
    }
  });
  await server.start();
  console.log('Server running at:', server.info.uri);
};

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

init();

to compare 2 version numbers with the somever.compare method.

Then we get -1 returned since the first version number is earlier than the 2nd one.

Also, we can compare versions with the match method:

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

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

  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        return somever.match('1.2.x', '1.2.5');
      },
    }
  });
  await server.start();
  console.log('Server running at:', server.info.uri);
};

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

init();

And we get true since '1.2.x' includes '1.2.5' .

Parsing HTTP Payloads

We can parse HTTP payload with the @hapi/subtext module.

For example, we can write:

const Http = require('http');
const Subtext = require('@hapi/subtext');

Http.createServer(async (request, response) => {
  const { payload, mime } = await Subtext.parse(request, null, { parse: true, output: 'data' });
  response.writeHead(200, { 'Content-Type': 'text/plain' });
  response.end(`Payload contains: ${JSON.stringify(payload)}`);

}).listen(1337, '0.0.0.0');

console.log('Server running/');

We call Subtext.parse with the request object to parse it.

It returns a promise with an object with the payload and mime properties.

payload has the payload sent.

mime has the MIME type.

Sorting Text

We can sort text with the @hapi/topo module.

To use it, we write:

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

const morning = new Topo.Sorter();

morning.add('Nap', { after: ['breakfast', 'prep'] });
morning.add([
  'Make toast',
  'Pour juice'
], { before: 'breakfast', group: 'prep' });
morning.add('Eat breakfast', { group: 'breakfast' });

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });
  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        return morning.nodes;
      },
    }
  });
  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We create the morning object with the Topo.Sorter constructor.

Then we call morning.add to add the items we want to sort.

The after property specifies the items that come after the item in the first argument.

group lets us add items into groups.

Then we can get the sorted items with the morning.nodes property.

Conclusion

We can sort items with the @hapi/topo module.

The @hapi/subtext module lets us parse request payloads.

And @hapi/scooter returns user agent info.

@hapi/somever lets us compare semantic version strings.

Categories
Hapi

Server-Side Development with Hapi.js — MIME Types and Events

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.

MIME Types

We can get information about various MIME types with the modules.

For instance, we can write:

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

const options = {
  override: {
    'node/module': {
      source: 'iana',
      compressible: true,
      extensions: ['node', 'module', 'npm'],
      type: 'node/module'
    },
    'application/javascript': {
      source: 'iana',
      charset: 'UTF-8',
      compressible: true,
      extensions: ['js', 'javascript'],
      type: 'text/javascript'
    },
    'text/html': {
      predicate(mime) {
        mime.foo = 'test';
        return mime;
      }
    }
  }
}

const mimos = new Mimos(options);

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

  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        return mimos.type('text/html');
      },
    }
  });

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

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

We add the options object with the override property to set the data for the given MIME types.

Then we call mimos.type with the MIME type we want to look up.

And then we get:

{"source":"iana","compressible":true,"extensions":["html","htm","shtml"],"type":"text/html","foo":"test"}

as the returned value.

Collect Server Ops Data

We can collect server ops data easily with the @hapi/oppsy module.

To use it, we can write:

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

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

  const oppsy = new Oppsy(server);
  oppsy.on('ops', (data) => {
    console.log(data);
  });

  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        return 'hello'
      },
    }
  });

  await server.start();
  oppsy.start(1000);
  console.log('Server running at:', server.info.uri);
};

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

We create the oppsy object with the Oppsy constructor.

Then we listen to the ops event with the oppsy.on method.

And in the event handler, we log the data from the server.

It includes data like requests, CPU usage, response times, memory usage, and more.

Events

We can create our own event bus with the @hapi/podium module.

For example, we can write:

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

const emitter = new Podium()
const context = { count: 0 }

emitter.registerEvent({
  name: 'event',
  channels: ['ch1', 'ch2']
})

const handler1 = function () {
  ++this.count
  console.log(this.count)
};

const handler2 = function () {
  this.count = this.count + 2
  console.log(this.count)
}

emitter.on({
  name: 'event',
  channels: ['ch1']
}, handler1, context);

emitter.on({
  name: 'event',
  channels: ['ch2']
}, handler2, context)

emitter.emit({
  name: 'event',
  channel: 'ch1'
})

emitter.emit({
  name: 'event',
  channel: 'ch2'
})

emitter.hasListeners('event')
emitter.removeAllListeners('event')
const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        return 'hello'
      },
    }
  });

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

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

We import the module, then we create the emitter object with it.

Then we register events with the emitter.registerEvent method.

We can separate events into their own channels.

context is the value of this in the event handlers.

So this.count will be updated and logged within the event handlers.

Conclusion

We can create our own event bus with @hapi/podium and handle MIME types with the @hapi/mimos module.

Categories
Hapi

Server-Side Development with Hapi.js — Tokens, JWT, and Secrets

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 Tokens

We can create tokens with the @hapi/iron module.

For example, we can write:

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

const obj = {
  a: 1,
  b: 2,
  c: [3, 4, 5],
  d: {
      e: 'f'
  }
};

const password = 'passwordpasswordpasswordpassword';

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

  server.route({
    method: 'GET',
    path: '/',
    async handler(request, h) {
      try {
        const sealed = await iron.seal(obj, password, iron.defaults);
        return sealed
      } catch (err) {
        console.log(err);
      }
    }
  });

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

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

We call the iron.seal method with the object we want to encrypt, the password to access the encrypted object, and the settings, which is iron.defaults .

Them the sealed string is a scrambled version of the object in string form.

Then to unseal it, we call the iron.unseal method.

For example, we can write:

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

const obj = {
  a: 1,
  b: 2,
  c: [3, 4, 5],
  d: {
      e: 'f'
  }
};

const password = 'passwordpasswordpasswordpassword';

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

  server.route({
    method: 'GET',
    path: '/',
    async handler(request, h) {
      try {
        const sealed = await iron.seal(obj, password, iron.defaults);
        const unsealed = await iron.unseal(sealed, password, iron.defaults);
        return unsealed
      } catch (err) {
        console.log(err);
      }
    }
  });

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

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

to decrypt the encrypted string with the same password with the iron.unseal method.

JSON Web Token

We can create and verify JSON web tokens with the @hapi/jwt module.

For example, we can use it by writing:”

const Hapi = require('@hapi/hapi');
const Jwt = require('@hapi/jwt');
const jwt = require('jsonwebtoken');

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

  await server.register(Jwt);

  server.auth.strategy('my_jwt_stategy', 'jwt', {
    keys: 'some_shared_secret',
    verify: {
      aud: 'urn:audience:test',
      iss: 'urn:issuer:test',
      sub: false,
      nbf: true,
      exp: true,
      maxAgeSec: 14400,
      timeSkewSec: 15
    },
    validate: (artifacts, request, h) => {
      return {
        isValid: true,
        credentials: { user: artifacts.decoded.payload.user }
      };
    }
  });

  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        const token = jwt.sign({
          aud: 'urn:audience:test',
          iss: 'urn:issuer:test',
          sub: false,
          maxAgeSec: 14400,
          timeSkewSec: 15
        }, 'some_shared_secret');
        return token;
      },

}
  });

  server.route({
    method: 'GET',
    path: '/secret',
    config: {
      handler(request, h) {
        return 'secret';
      },
      auth: {
        strategy: 'my_jwt_stategy',
      }
    }
  });

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

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

We add the jsonwebtoken module to create the token.

Then we call server.auth.strategy to add the JWT auth strategy.

The keys have the keys we use to verify the token.

verify has the fields we want to verify.

validate has a function that returns isValid to indicate that the token is valid.

credentials have the data from the decoded token.

Then we call jwt.sign to sign the token in the / route handler.

And in the /secret route, we have the auth.strategy property to set the auth strategy.

Conclusion

We can create various kinds of tokens and verify them with Hapi addons.

Categories
Hapi

Server-Side Development with Hapi.js — File Names, Composing Server Parts, and Process Monitor

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.

File Utility

We can generate a file path from path segments with the @hapi/file module.

For example, we can write:

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

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const fileName = file.uniqueFilename('/root', '.txt');
      return fileName
    }
  });

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

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

We call the file.uniqueFilename method to create a unique file name.

We should get something like:

/root/1604965754941-1144-2fbcec5c8927e564.txt

returned as the response.

Compose Different Server Components

We can use the @hapi/glue module to compose different server components.

To use it, we write:

index.js

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

const manifest = {
  server: {
    port: 3000,
    host: '0.0.0.0'
  },
  register: {
    plugins: [
      {
        plugin: require('./my-plugin'),
        options: {
          uglify: true
        }
      },
    ],
  }
};

const options = {
  relativeTo: __dirname
};

const init = async () => {
  const server = await Glue.compose(manifest, options);

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

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

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

my-plugin.js

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

module.exports = myPlugin

We created a plugin with one route within the my-plugin.js file.

Then in index.js , we have our manifest object with the server options.

register is a property with the plugin properties inside.

The plugins array has an array of plugins we want to register.

options has extra options we want to set for our server.

Then we compose everything together with the Glue.compose method.

It returns the Hapi server object which we can use to add more routes with the server.route method.

Process Monitor

We can add a simple process monitor to our Hapi app with the @hapi/good module.

To add it, we write:

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

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

  await server.register({
    plugin: require('@hapi/good'),
    options: {
      ops: {
        interval: 1000
      },
      reporters: {
        myConsoleReporter: [
          {
            module: '@hapi/good-squeeze',
            name: 'Squeeze',
            args: [{ log: '*', response: '*', ops: '*' }]
          },
          {
            module: '@hapi/good-console'
          },
          'stdout'
        ]
      }
    }
  });

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

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

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

We add the reporters property to add a logger for monitoring resource usage.

We add the @hapi/good-squeeze module to add the items to log with the args array.

log has the timestamp, response has the HTTP response returned, and ops has the resource usage.

The @hapi/good-console lets us log the numbers to the console.

Conclusion

We can add various modules to our Hapi app to monitor resource usage, generate file names, and compose server components.