Routing

This tutorial is compatible with hapi v17 and newer

Overview

When defining a route in hapi, you need three basic elements: the path, the method, and a handler. These are passed to your server as an object, and can be as simple as the following:

server.route({
    method: 'GET',
    path: '/',
    handler: function (request, h) {

        return 'Hello World!';
    }
});

Methods

The route above responds to a GET request to / with the string Hello World!. The method option can be any valid HTTP method, or an array of methods. Let's say you want the same response when your user sends either a PUT or a POST request, you could do that with the following:

server.route({
    method: ['PUT', 'POST'],
    path: '/',
    handler: function (request, h) {

        return 'I did something!';
    }
});

Path

The path option must be a string, though it can contain named parameters. To name a parameter in a path, simply wrap it with {}. For example:

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

        return `Hello ${request.params.user}!`;
    }
});

Note: It is best practice to always return escaped and validated user inputs such as query/path parameters. This is done to prevent echo or XSS attacks. One way to do this is to use Hoek escapeHtml() method. With escaping in place, the above example would look like the following:

return `Hello ${Hoek.escapeHtml(request.params.user)}!`

As you can see above, you have the string {user} in your path, which means you're asking for that segment of the path to be assigned to a named parameter. These parameters are stored in the object request.params within the handler. Since you named your parameter user, you are able to access the value with the property request.params.user, after URI encoding it so as to prevent content injection attacks. For example, going to /hello/ferris in your browser, you will see Hello ferris!.

Optional Parameters

In the above example, the user parameter is required: a request to /hello/bob or /hello/susan will work, but a request to /hello will not. In order to make a parameter optional, put a question mark at the end of the parameter's name. Here is the same route, but updated to make the user parameter optional:

server.route({
    method: 'GET',
    path: '/hello/{user?}',
    handler: function (request, h) {

        const user = request.params.user ? request.params.user : 'stranger';

        return `Hello ${user}!`;
    }
});

Now a request to /hello/sloan will reply with Hello sloan! and a request to just /hello will reply with Hello stranger!. It is important to be aware that only the last named parameter in a path can be optional. That means that /{one?}/{two}/ is an invalid path, since in this case there is another parameter after the optional one. You may also have a named parameter covering only part of a segment of the path for instance /{filename}.jpg is valid. You may also have multiple parameters per segment provided there is non-parameter separator between them, meaning /{filename}.{ext} is valid whereas /{filename}{ext} is not.

Multi-Segment Parameters

Along with optional path parameters, you can also allow parameters that match multiple segments. In order to do this, you use an asterisk and a number. For example:

server.route({
    method: 'GET',
    path: '/hello/{user*2}',
    handler: function (request, h) {

        const userParts = request.params.user.split('/');

        return `Hello ${userParts[0]} ${userParts[1]}!`;
    }
});

With this configuration, a request to /hello/john/doe will reply with the string Hello john doe!. The important thing to note here is that the parameter is actually the string "john/doe". That's why you did a split on that character to get the two separate parts. The number after the asterisk represents how many path segments should be assigned to the parameter. You can also omit the number entirely, and the parameter will match any number of segments available. Like the optional parameters, a wildcard parameter (for example /{files*}) may only appear as the last parameter in your path.

When determining what handler to use for a particular request, hapi searches paths in order from most specific to least specific. That means if you have two routes, one with the path /filename.jpg and a second route /filename.{ext} a request to /filename.jpg will match the first route, and not the second. This also means that a route with the path /{files*} will be the last route tested, and will only match if all other routes fail.

Query Parameters

Query parameters are common way of sending data to the server. Query parameters are sent via the URL in a key=value format. For example:

localhost:3000?name=ferris&location=chicago

There are two query parameters here, name=ferris and location=chicago. In hapi, you can access query parameters by the request.query object.

server.route({
    method: 'GET',
    path: '/',
    handler: function (request, h) {

        return `Hello ${request.query.name}!`;
    }
});

Here, you simply access the name query parameter and return it in the handler, which would read Hello ferris!.

For more complex query structures, you may opt to use the qs module. Consider the following:

server.route({
    method: 'GET',
    path: '/',
    handler: function (request, h) {

        return request.query;
    }
});

If you sent the request localhost:3000?foo[bar]=baz, hapi, by default would return { "foo[bar]": "baz" }.

With the qs module, you can parse the query string out. An example:

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

const server = Hapi.server({
    port: 3000,
    host: 'localhost',
    query: {
        parser: (query) => Qs.parse(query)
    }
});

server.route({
    method: 'GET',
    path: '/',
    handler: function (request, h) {

        return request.query;
    }
});

const init = async () => {

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

process.on('unhandledRejection', (err) => {

    console.log(err);
    process.exit(1);
});

init();

Here, you first require the qs module.

Second, you set the query parameters parser method by setting the server.options.query.parser value. In this case, you use the Qs.parse() method where query is an object containing the incoming request.query parameters. Now, anything coming into request.query will be parsed with Qs.parse().

Lastly, you returned the parsed query string, which would now be:

{
    "foo": {
        "bar": "baz"
    }
}

Note: In the above example, you used the qs module to handle our parsing, but any parser will do, be it from npm or even custom. Just be aware that the method must return an object where each key is a parameter and matching value is the parameter value.

Request Payload

Anytime you send request data to your API, you will be able to access this data in the route handler with request.payload. See the following:

server.route({
    method: 'POST',
    path: '/signup',
    handler: function (request, h) {

        const payload = request.payload;

        return `Welcome ${payload.username}!`;
    }
});

In the above example, the handler receives data via request.payload. In this case, the request.payload contains an object that stores user sign up data:

{ username: 'ferris', password: 'password' }

Note: It's always best practice to validate the incoming payload, as it may contain unsafe data. See validation tutorial for more info.

Handler

The handler option is a function that accepts two parameters, request, and h.

The request parameter is an object with details about the end user's request, such as path parameters, an associated payload, query parameters, authentication information, headers, etc. Full documentation on what the request object contains can be found in the API reference.

The second parameter, h, is the response toolkit, an object with several methods used to respond to the request. As you've seen in the previous examples, if you wish to respond to a request with some value, you simply return it from the handler. The payload may be a string, a buffer, a JSON serializable object, a stream or a promise.

Alternatively you may pass the same value to h.response(value) and return that from the handler. The result of this call is a response object, that can be chained with additional methods to alter the response before it is sent. For example h.response('created').code(201) will send a payload of created with an HTTP status code of 201. You may also set headers, content type, content length, send a redirection response, and many other things that are documented in the API reference.

The handler option must return a value, a promise, or throw an error.

Note: handlers using a fat arrow style function cannot be bound to any server.bind() property. Instead, the bound context is available under h.context.

Options

Aside from these three basic elements, you may also specify an options parameter for each route. This is where you configure things like validation, authentication, prerequisites, payload processing, and caching options. More details can be found in the linked tutorials, as well as the API reference.

Here we will look at some options of validating with Joi.

server.route({
    method: 'POST',
    path: '/signup',
    handler: function (request, h) {

        const payload = request.payload;

        return `Welcome ${payload.username}!`;
    },
    options: {
        auth: false,
        validate: {
            payload: {
                username: Joi.string().min(1).max(20),
                password: Joi.string().min(7)
            }
        }
    }
});

The first property under options is auth. auth will set the authentication configuration for the route. Since this route is for a new user signing up, you will disable authentication.

The second property is validate. This allows you to set validation rules for various request components, such as headers, params, payload, and failAction. You use the joi package to validate the request.payload. For more info, please check the validation tutorial.

404 Handling

404 errors will happen whenever your server can't find what was the resource that was requested. It is best practice to handle these errors the proper way. This is easy to do in hapi, by just employing a route that will catch everything your other routes will not. The following example shows how to setup a route to return a custom 404 response.

'use strict';

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

const internals = {};

const init = async () => {

    const server = Hapi.server({
        port: 3000,
        host: 'localhost'
    });

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

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

init();

First, you configure our server.

Next, you setup your route that return your custom 404 response. You use a wildcard, *, for the method, so it covers all available methods. Then, you use a very broad, generic path, '/{any*}. This will catch any route that our other routes do not. hapi routes will go the the most specific path first, then get broader, till it finds a match. For example, localhost:3000/login will go to the /login route and not the /{any*} route.

Lastly, you return a custom 404 response in your handler, letting your users know that the resource they are asking for could not be found.

clipboard