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.

Categories
Hapi

Server-Side Development with Hapi.js — Server Method Cache

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.

Cache

We can cache method results with Hapi.

This is a great benefit that doesn’t come with regular functions.

For example, we can write:

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

const add = (x, y) => {
  return x + y;
};

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

  server.method('add', add, {
    cache: {
      expiresIn: 60000,
      staleIn: 30000,
      staleTimeout: 10000,
      generateTimeout: 100
    }
  });

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

  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 cache property with the expiresIn property to set the expiry time. The time is in milliseconds.

staleIn sets when the data is invalidated. The time is in milliseconds.

stateTimeout sets the number of milliseconds before returning a stale value while a fresh value is generated.

generateTimeout is the number of milliseconds to wait before returning a timeout error.

We can replace expiresIn with expiresAt :

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

const add = (x, y) => {
  return x + y;
};

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

  server.method('add', add, {
    cache: {
      expiresAt: '20:30',
      staleIn: 30000,
      staleTimeout: 10000,
      generateTimeout: 100
    }
  });

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

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

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

init();

expiresAt is the time of day expressed in 24h notation in HH:MM format.

It’s time art which the cache records for the route expires.

It can’t be used together with expiresIn .

We can override the ttl of as a server per-invocation by setting the flags.ttl property.

For example, we can write:

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

const add = (x, y, flags) => {
  flags.ttl = 5 * 60 * 1000;
  return x + y;
};

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

  server.method('add', add, {
    cache: {
      expiresAt: '20:30',
      staleIn: 30000,
      staleTimeout: 10000,
      generateTimeout: 100
    }
  });

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

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

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

init();

to set the flags.ttl property to the timeout we want.

Generate a Custom Key

We can create a custom cache key for the cached results.

To do this, we write:

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

const add = (arr) => {
  return arr.reduce((a, b) => a + b);
};

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

  server.method('add', add, {
    generateKey: (array) => array.join(',')
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      return server.methods.add([1, 2, 3]);
    }
  });

  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 the generateKey method to generate a unique cache key with our own function.

Conclusion

We can change various cache options with Hapi server methods.

Categories
Hapi

Server-Side Development with Hapi.js — 404 Handling and Shared Methods

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.

404 Handling

We can handle 404 errors easily with Hapi.

For example, we can write:

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

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

  server.route({
    method: '*',
    path: '/{any*}',
    handler (request, h) {
        return '404 Error! Page Not Found!';
    }
  });

  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 method to '*' to allow for all methods.

path is set to ‘/{any*}’ to allow the route handler to watch for any path.

Server Methods

The server.method method lets us add methods to our app.

Once we add the method, we can share them throughout the whole app.

For instance, we can write:

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

const add = (x, y) => {
  return x + y;
};

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

  server.method('add', add, {});

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

  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 the add function that adds 2 numbers.

Then we call the server.method method with it to register the method.

The first argument is the name. The 2nd argument is the function reference.

The 3rd argument is options, we can pass in.

Then we called it in the handler with server.methods.add(1, 2); .

So we get 3 as the response if we go to the / route.

We can also write:

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

const add = (x, y) => {
  return x + y;
};

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

  server.method({
    name: 'add',
    method: add,
    options: {}
  });

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

  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.

server.method is called with an object with the same data but they are property values instead of arguments.

Also, we can namespace our methods with a namespace string as the first argument.

For instance, we can write:

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

const add = (x, y) => {
  return x + y;
};

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

  server.method('math.add', add);

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

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

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

init();

We register our math.add method with:

server.method('math.add', add);

Then we call it with:

server.methods.math.add(1, 2)

Conclusion

We can handle 404 errors with a route and also add app-wide methods with server.methods with Hapi.