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.

Categories
Hapi

Server-Side Development with Hapi.js — 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.

Views

We can render templates in our Hapi app.

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: require('handlebars')
    },
    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

<p>hello</p>

We register the Hapi Vision plugin to let us render views:

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

Then we set the view options with:

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

engines specify the view engine to use.

relativeTo sets the path that the templates are stored relative to.

path is the path to the templates.

We render the view with:

return h.view('index');

We specify the file name of the template to render it.

So when we to the / we see hello rendered.

Other paths we can specify include:

  • partialsPath: the folder that contains our partials
  • helpersPath: the folder that contains our template helpers
  • layoutPath: the folder that contains layout templates

So we can write:

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: require('handlebars')
    },
    relativeTo: __dirname,
    path: 'templates',
    layoutPath: './templates/layout',
    helpersPath: './templates/helpers'
  });

  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();

to specify the paths.

Global Context

We can pass in data by passing in the context object to the server.views call.

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',
    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/index.html

<p>{{title}}</p>

We have the context object.

And we pass that into the object we call server.views as its property.

In templates/index.html , we render the title in the template.

So we see:

My personal site

displayed when we go to / .

Conclusion

We can render views with the Hapi Vision plugin.

Categories
Hapi

Server-Side Development with Hapi.js — Response Validation

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.

Validate Responses

We can validate responses with Hapi.

For example, we can write:

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

const bookSchema = Joi.object({
  title: Joi.string().required(),
  author: Joi.string().required(),
  isbn: Joi.string().length(10),
  pageCount: Joi.number(),
  datePublished: Joi.date().iso()
});

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) {
      return [
        {
          title: 'book',
          author: 'john smith',
          isbn: '1234567890',
          pageCount: 100,
          datePublished: '2020-02-01'
        }
      ];
    },
    options: {
      response: {
        schema: Joi.array().items(bookSchema),
        failAction: 'log'
      }
    }
  });
  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 bookSchema with the Joi.object method.

It lets validate the objects we have in our response.

We specify that we have the title , author , isbn , pageCount and datePublished fields.

Joi.string().required() makes the title and author field required.

isbn is set to have a string with length by writing:

Joi.string().length(10)

pageCount is number and datePublished is set to have a YYYY-MM-DD format.

We pass that into Joi.array().items() so that we specify that each entry of the array follows the schema.

failAction is 'log' so when validation fails, the failure is logged.

We enabled logging with:

debug: { request: ['error'] }

so we can see the error.

We can choose to only validate a percentage of the responses.

For instance, we can write:

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

const bookSchema = Joi.object({
  title: Joi.string().required(),
  author: Joi.string().required(),
  isbn: Joi.string().length(10),
  pageCount: Joi.number(),
  datePublished: Joi.date().iso()
});

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) {
      return [
        {
          title: 'book',
          author: 'john smith',
          isbn: '1234567890',
          pagecount: 100,
          datePublished: '2020-02-01'
        }
      ];
    },
    options: {
      response: {
        sample: 50,
        schema: Joi.array().items(bookSchema),
        failAction: 'log'
      }
    }
  });
  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We set options.response.sample to 50 to validate only the first 50 entries.

Validate Status

We validate the response we return according to the response status.

For example, we can write:

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

const dataSchema = Joi.object({
  title: Joi.string().required(),
  author: Joi.string().required(),
});

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 data = {
        title: 'book',
        author: 'john smith',
      }
      return h.response(data).code(201)
    },
    options: {
      response: {
        status: {
          201: dataSchema,
          202: Joi.object({
            original: dataSchema,
            updated: dataSchema
          })
        }
      }
    }
  });
  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 dataSchema with the schema.

Then we set dataSchema as the values of the 201 property.

And we put that inside the Joi.object call for the 202 property.

Since we returned a valid response within the request handler, we get no error in the log.

If we don’t follow the schema, then we get an error logged.

Conclusion

We can validate responses and log validation errors with Hapi and Joi.