Categories
Hapi

Server-Side Development with Hapi.js — Throwing Errors

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.

Throw Unauthorized Errors

We can throw unauthorized errors with the @hapi/boom module.

It has the Boom.unauthorized method to throw the error.

For instance, we can write:

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

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      throw Boom.unauthorized('invalid password');
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call Boom.unauthorized method with a message to create the 401 response.

Now we should get:

{"statusCode":401,"error":"Unauthorized","message":"invalid password"}

when we make a GET request to the / route.

We can pass in extra data in the 3rd argument.

For example, we can write:

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

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      throw Boom.unauthorized('invalid password', 'sample', { ttl: 0, cache: null, foo: 'bar' });
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

The object in the 3rd argument will be added to the attributes property of the response.

So we get:

{"statusCode":401,"error":"Unauthorized","message":"invalid password","attributes":{"ttl":0,"cache":"","foo":"bar","error":"invalid password"}}

as the response.

The 2nd argument will be in the Www-Authenticate response header.

We get:

sample ttl="0", cache="", foo="bar", error="invalid password"

as its value.

Payment Required Response

We can return a 402 response with the Boom.paymentRequired method.

For instance, we can write:

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

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      throw Boom.paymentRequired('bandwidth used');
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call Boom.paymentRequied with the message that we want to return.

Then we get:

{"statusCode":402,"error":"Payment Required","message":"bandwidth used"}

as the response.

Forbidden Error

We can throw a forbidden error with the Boom.forbidden method:

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

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      throw Boom.forbidden('try again some time');
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

Then we get:

{"statusCode":403,"error":"Forbidden","message":"try again some time"}

as the response.

Not Found Error

We can return a 404 error with the Boom.notFound method:

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

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      throw Boom.notFound('missing');
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

Then we should get:

{"statusCode":404,"error":"Not Found","message":"missing"}

as the response.

Conclusion

We can throw various kinds of errors easily with the @hapi/boom module.

Categories
Hapi

Server-Side Development with Hapi.js — Base64 and Errors

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.

Base64 Encoding

We can handle base64 encoding and decoding with the @hapi/b64 module.

For instance, we can write:

const Path = require('path');
const Hapi = require('@hapi/hapi');
const Fs = require('fs');
const B64 = require('@hapi/b64');

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const stream = Fs.createReadStream(Path.join(__dirname, 'package.json'));
      const encoder = new B64.Encoder();
      stream.pipe(encoder).pipe(process.stdout);
      return 'success'
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call Fs.createReadStream to create the srean.

Then we create the encoder object with the B64.Encoder constructor.

And then we pass the encoder to the stream.pipe method to encode the stream into a base64 string.

And then we pass that into stdout with another pipe call.

Base64 Decoding

We can decode base64 text into its original content with the @hapi/b64 module.

To do this, we write:

const Path = require('path');
const Hapi = require('@hapi/hapi');
const Fs = require('fs');
const B64 = require('@hapi/b64');

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const stream = Fs.createReadStream(Path.join(__dirname, 'encodedfile.b64'));
      const decoder = new B64.Decoder();
      stream.pipe(decoder).pipe(process.stdout);
      return 'success'
    }
  });

  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 Fs.createReadStream to create the read stream and read the file with the given path.

Then we create the decoder object with the B64.Decoder constructor.

And then we call stream.pipe with the decoder to do the decoding.

Then we call pipe again to pipe the decoded content to stdout .

Error Response

We can add a login route easily with the @hapi/boom module.

To use it, we write:

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

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      throw Boom.badRequest('invalid query');
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call Boom.badRequest to create an Error object that we can throw.

It takes the error message as the argument.

Then we throw that and we should see:

{"statusCode":400,"error":"Bad Request","message":"invalid query"}

returned as the response.

We can check if an object is an instance of the Boom.Boom constructor by writing:

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

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const err = Boom.badRequest('invalid query');
      return Boom.isBoom(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 Boom.isBoom to do the check.

It should return true since err is an instance of Boom.Boom .

Conclusion

We can return error responses easily with the @hapi/boom module.

The @hapi/base64 module lets us encode and decode base64 content.

Categories
Hapi

Server-Side Development with Hapi.js — Handling Accept Headers

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.

Parse Accept Header

We can parse the Accept header with the @hapi/accept module.

For example, we can write:

const Path = require('path');
const Hapi = require('@hapi/hapi');
const Accept = require('@hapi/accept');

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const charset = Accept.charsets("iso-8859-5, unicode-1-1;q=0.8");
      return charset
    }
});

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call Accept.charsets to return an array of encodings.

charset is [“iso-8859–5”,”unicode-1–1"] .

The Accept-Encoding header is checked to get the returned value

We can use this array in our requests handler to check the preferred encoding.

We can pass in a 2nd argument with the values we want to compare against the string in the first argument:

const Path = require('path');
const Hapi = require('@hapi/hapi');
const Accept = require('@hapi/accept');

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const encoding = Accept.encoding("gzip, deflate, sdch", ["deflate", "identity"]);
      return encoding
    }
});

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We have the deflate in the string in the first argument and 'deflate' in the 2nd argument, so it’ll return 'deflate' .

We can do the same comparisons for language.

For instance, we can write:

const Path = require('path');
const Hapi = require('@hapi/hapi');
const Accept = require('@hapi/accept');

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const language = Accept.language("en;q=0.7, en-GB;q=0.8");
      return language
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We pass in a string. Then it returns the string according to the value of the Accept-Language header.

Also, we can write:

const Path = require('path');
const Hapi = require('@hapi/hapi');
const Accept = require('@hapi/accept');

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

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const language = Accept.language("en;q=0.7, en-GB;q=0.8", ["en-gb"]);
      return language
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

to do the comparison with the value in the array.

Since it has 'en-gb' as its only entry, this is returned.

Conclusion

We can parse and check the Accept-Encoding and Accept-Language header with the @hapi/accept module.

Categories
Hapi

Server-Side Development with Hapi.js — Rendering Views

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 a View

We can render a view with the h.view method.

For example, we can write:

index.js

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

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0',
    debug: { request: ['error'] }
  });
  await server.register(require('@hapi/vision'));
  server.views({
    engines: {
      html: {
        module: require('handlebars'),
        compileMode: 'sync'
      },
    },
    relativeTo: __dirname,
    path: 'templates',
  });

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

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

templates/index.html

<div>Content</div>

We have:

h.view('index')

to render the index.html file.

And we have the relativeTo property to specify where the template folder is relative to.

And path has the template folder name.

So whatever is in the index.html file is rendered.

We can pass in dynamic data to the view. For instance, we can write:

index.js

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

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0',
    debug: { request: ['error'] }
  });
  await server.register(require('@hapi/vision'));
  server.views({
    engines: {
      html: {
        module: require('handlebars'),
        compileMode: 'sync'
      },
    },
    relativeTo: __dirname,
    path: 'templates',
  });

  server.route({
    method: 'GET',
    path: '/',
    handler (request, h) {
      return h.view('index', { title: 'My home page' });
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

templates/index.html

<div>{{title}}</div>

We call h.view with the 2nd argument:

h.view('index', { title: 'My home page' })

The title property is rendered with {{title}} .

View Handler

We can shorten our route handler that render views with a view handler.

For example, we can write:

index.js

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

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0',
    debug: { request: ['error'] }
  });
  await server.register(require('@hapi/vision'));
  server.views({
    engines: {
      html: {
        module: require('handlebars'),
        compileMode: 'sync'
      },
    },
    relativeTo: __dirname,
    path: 'templates',
  });

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

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We have:

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

to specify the view file to render instead of calling h.view .

To pass in data to the view, we write:

index.js

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

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0',
    debug: { request: ['error'] }
  });
  await server.register(require('@hapi/vision'));
  server.views({
    engines: {
      html: {
        module: require('handlebars'),
        compileMode: 'sync'
      },
    },
    relativeTo: __dirname,
    path: 'templates',
  });

  server.route({
    method: 'GET',
    path: '/',
    handler: {
      view: {
        template: 'index',
        context: {
          title: 'My home page'
        }
      }
    }
});

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

templates/index.html

<div>{{title}}</div>

We set the context.title property to pass the title to the view.

Then we render that with {{title}} .

So when we go to / , we see My home page displayed.

Conclusion

We can render views with view handlers and pass in dynamic data to Hapi views.

Categories
Hapi

Server-Side Development with Hapi.js — View Helpers and Layouts

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.

View Helpers

We can add view helpers to our app.

For example, we can write:

index.js

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

const context = {
  title: 'My personal site'
};

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0',
    debug: { request: ['error'] }
  });
  await server.register(require('@hapi/vision'));
  server.views({
    engines: {
      html: {
        module: require('handlebars'),
        compileMode: 'sync'
      },
    },
    relativeTo: __dirname,
    path: 'templates',
    helpersPath: './templates/helpers',
    context
  });

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

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

templates/helpers/fortunes.js

module.exports = () => {
  const fortunes = [
    'Wanna buy a duck?',
    'Say no, then negotiate.',
    'Time and tide wait for no man.',
    'To teach is to learn.',
    'Never ask the barber if you need a haircut.',
    'You will forget that you ever knew me.',
    'You will be run over by a beer truck.',
    'Fortune favors the lucky.',
    'Have a nice day!'
  ];

  const x = Math.floor(Math.random() * fortunes.length);
  return fortunes[x];
};

We create the fortune.js which has a function we exported.

It returns the value we want to display.

In index.html , we interpolate the value of the helper name to displayed the returned value:

<p>{{fortune}}</p>

And then we specify the path to the template helpers with the helpersPath property.

Now when we go to the / page, we see a random fortune displayed.

Layouts

We can add our layouts in their own files.

For example, we can write:

index.js

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

const context = {
  title: 'My personal site'
};

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0',
    debug: { request: ['error'] }
  });
  await server.register(require('@hapi/vision'));
  server.views({
    engines: {
      html: {
        module: require('handlebars'),
        compileMode: 'sync'
      },
    },
    relativeTo: __dirname,
    path: 'templates',
    layout: true,
    layoutPath: 'templates/layout',
  });

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

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

templates/layout/layout.html

<html>
  <body>
    {{{content}}}
 </body>
</html>

templates/index.html

<div>Content</div>

We set the layout property to true to let us use layout files for layouts.

Then we set the layoutPath to set the layout path.

In layout.html , we have:

{{{content}}}

to display the content.

And in templates/index.html , we have the content.

We can also specify a different layout per view.

For instance, we can write:

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

const context = {
  title: 'My personal site'
};

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0',
    debug: { request: ['error'] }
  });
  await server.register(require('@hapi/vision'));
  server.views({
    engines: {
      html: {
        module: require('handlebars'),
        compileMode: 'sync'
      },
    },
    relativeTo: __dirname,
    path: 'templates',
    layout: true,
    layoutPath: 'templates/layout',
  });

  server.route({
    method: 'GET',
    path: '/',
    handler (request, h) {
      return h.view('index', null, { layout: 'another_layout' });
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

templates/layout/another_layout.html :

<html>
  <body>
    <b>{{{content}}}</b>
 </body>
</html>

We specify the layout we want to use by writing:

h.view('index', null, { layout: 'another_layout' });

The layout property has the name of the layout file we want to use.

Conclusion

We can render layouts and helpers in views with Hapi.