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.

Categories
Hapi

Server-Side Development with Hapi.js — Validating Requests

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.

Query Parameters

We can validate query parameters with Hapi and Joi.

For example, we can write:

const Joi = require('@hapi/joi');  
const Hapi = require('@hapi/hapi');  
const init = async () => {  
  const server = new Hapi.Server({  
    port: 3000,  
    host: '0.0.0.0',  
  });  
  server.route({  
    method: 'GET',  
    path: '/',  
    handler(request, h) {  
      return request.query.limit;  
    },  
    options: {  
      validate: {  
        query: Joi.object({  
          limit: Joi.number().integer().min(1).max(100).default(10)  
        })  
      }  
    }  
  });  
  await server.start();  
  console.log('Server running at:', server.info.uri);  
};  
process.on('unhandledRejection', (err) => {  
  console.log(err);  
  process.exit(1);  
});  
init();

We set the validate.query property to the object returned with Joi.object .

Then we specify the validation rules for the limit query parameter inside it.

We specified that limit is an integer between 1 and 100 by writing:

Joi.number().integer().min(1).max(100).default(10)

Payload Parameters

Also, we can validate payload parameters by setting the validate.payload property.

For instance, we can write:

const Joi = require('@hapi/joi');  
const Hapi = require('@hapi/hapi');  
const init = async () => {  
  const server = new Hapi.Server({  
    port: 3000,  
    host: '0.0.0.0',  
  });  
  server.route({  
    method: 'POST',  
    path: '/',  
    handler(request, h) {  
      return request.payload;  
    },  
    options: {  
      validate: {  
        payload: Joi.object({  
          post: Joi.string().min(1).max(140),  
          date: Joi.date().required()  
        })  
      }  
    }  
  });  
  await server.start();  
  console.log('Server running at:', server.info.uri);  
};  
process.on('unhandledRejection', (err) => {  
  console.log(err);  
  process.exit(1);  
});  
init();

We set the validate.payload property to an object returned by Joi.object to validate the payload with the properties inside.

post is a string and date is a required date field. It must be in the MM-DD-YYYY format.

So when we make a POST request to the / route with a body like:

{  
    "post": "1",  
    "date": "02-01-2020"  
}

Then we get the payload returned as the response.

Otherwise, we’ll get a 400 response.

Headers

We can also validate headers with Joi in our Hapi app.

For example, we can write:

const Joi = require('@hapi/joi');  
const Hapi = require('@hapi/hapi');  
const init = async () => {  
  const server = new Hapi.Server({  
    port: 3000,  
    host: '0.0.0.0',  
  });  
  server.route({  
    method: 'GET',  
    path: '/',  
    handler(request, h) {  
      return request.state;  
    },  
    options: {  
      validate: {  
        headers: Joi.object({  
          cookie: Joi.string().required()  
        }),  
        options: {  
          allowUnknown: true  
        }  
      }  
    }  
  });  
  await server.start();  
  console.log('Server running at:', server.info.uri);  
};  
process.on('unhandledRejection', (err) => {  
  console.log(err);  
  process.exit(1);  
});  
init();

We set the validate.headers property by setting that to the object returned byJoi.object .

Inside that, we set the cookie property to a required string with:

Joi.string().required()

Then we get the cookie with request.state in our request handler.

So if we send a Cookie header with value data=foo , we get:

{  
    "data": "foo"  
}

as the response.

Conclusion

We can validate various kinds of request content with Hapi and Joi.

Categories
Hapi

Server-Side Development with Hapi.js — Static File Directory and Input 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.

Directory Handler Options

We can configure how static files are served with some options.

For instance, we can write:

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

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

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

  server.route({
    method: 'GET',
    path: '/{param*}',
    handler: {
      directory: {
        path: Path.join(__dirname, 'public'),
        index: ['pic.png']
      }
    }
  });

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

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

init();

We set the index option to set the file to serve if we don’t have any value set for the param URL parameter.

Static File Server

We can create a static file server with Hapi.

For example, we can write:

const Path = require('path');
const Hapi = require('@hapi/hapi');
const Inert = require('@hapi/inert');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0',
    routes: {
      files: {
        relativeTo: Path.join(__dirname, 'public')
      }
    }
  });

  await server.register(Inert);

  server.route({
    method: 'GET',
    path: '/{param*}',
    handler: {
      directory: {
        path: '.',
        redirectToSlash: true
      }
    }
  });

  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 server with the routes.files.relativeTo option to specify the static files directory.

Then we call server.route to create a route to expose the static files.

The handler.directory.path property has the root path of the static files.

And redirectToSlash set to true means we treat the path with a trailing slash and no trailing slash to be the same.

Input Validation

We can add validation to our route to validate request data.

For example, we can write:

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

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

  server.route({
    method: 'GET',
    path: '/hello/{name}',
    handler (request, h) {
      return `Hello ${request.params.name}!`;
    },
    options: {
      validate: {
        params: Joi.object({
          name: Joi.string().min(3).max(10)
        })
      }
    }
  });

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

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

init();

to add validation.

We add the @hapi/joi module.

Then we add the options.validate.params property to add our validation for the URL request parameter.

We set the params property to the object returned by Joi.object call to validate the URL parameter.

Then inside it, we specify the parameters we want to validate.

The name property is set to Joi.string().min(3).max(10) so that we make sure it’s a string with a min length of 3 and max length of 10.

If the name parameter value doesn’t meet this requirement, then it’ll be rejected with a 400 response.

Conclusion

We can validate request URL parameters with @hapi/joi .

Also, we can serve static file directories with @hapi/inert .

Categories
Hapi

Server-Side Development with Hapi.js — this and Server Methods and Static Files

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.

Bind

We can change the value of this inside our server method with the bind option.

For example, we can write:

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

const log = function() {
  return this.foo
};

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

  server.method('log', log, { bind: { foo: 'bar' } });

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

  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 server.method method with the 3rd argument with the bind property.

Whatever the value of bind is the value of this inside the log function.

So when we call server.methods.log , we have 'bar' returned since it’s the value of this.foo .

And when we make a GET request to the / route, we get bar returned as the response.

Serving Static Content

We can serve static content with the @hapi/inert module.

For example, we can write:

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

const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0',
    routes: {
      files: {
        relativeTo: Path.join(__dirname, 'public')
      }
    }
  });

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

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

  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 server with the routes.files.relativeTo option.

We set the relativeTo property with the Path.join method to get the path to our static files.

Then we register the @hapi/inert plugin with:

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

And we in our route handler, we get render the static file by using the h.file method.

Now when we go to the / route, we see the pic.png image we uploaded to the public folder.

File Handler

We can also use the file handler to serve our static file by writing:

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

const init = async () => {
  const server = Hapi.server({
    port: 3000,
    host: '0.0.0.0',
    routes: {
      files: {
        relativeTo: Path.join(__dirname, 'public')
      }
    }
  });

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

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

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

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

init();

We replaced the handler method with an object with the path to our static file.

Directory Handler

Also, we can serve files from a directory by using the directory handler.

For instance, we can write:

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

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

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

  server.route({
    method: 'GET',
    path: '/{param*}',
    handler: {
      directory: {
        path: Path.join(__dirname, 'public')
      }
    }
  });

  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 ‘/{param*}’ path to let us pass in the file name of the file to get.

Then we set the handler.directory.path property to set the path which has the static files.

Conclusion

We can change the value of this in a server method with the bind property.

And we can serve static files and directories with the @hapi/inert module.