Oscar Funes

Using hapi route prerequisites

May 24, 2020

When registering a route in a Hapi Server, one the available options is named pre. At first it didn’t ring a bell for me, but upon reading the documentation I realized that they’re prerequisites for a route handler.

What are prerequisites (or pre) configurations?

Prerequisites accomplish different goals, but they’re mean to run prior to your handler, this way your handler ends up being more focused. Imagine fetching a resource for a database, or from another API, instead of using your handler for doing that fetch operation, you can create a prerequisite for it.

How can you add such a prerequisite?

server.route({
  method: 'get',
  route: '/a',
  handler(request, h) {
    // if you use `assign` property, you'll see the assign value as a property
    // in the `pre` object
    const { content } = request.pre
    return 'ok'
  },
  options: {
    pre: [
      {
        assign: 'content',
        method: async (request, h) => {
          // fetching content
          return server.methods.getContent('main_page')
        },
      },
    ],
  },
})

A prerequisite can either be a lifecycle method, or an object with 3 properties

  1. assign
  2. method
  3. failAction

If you setup the assign property, if the method returns a value, response or throw the value would be assigned to the property name, when failAction is not error.

If you check the documentation, you’ll see the mention of 2 properties:

  1. request.pre
  2. request.preResponses

If your prerequisite returns a response, e.g.: h.response({ data: ‘ok’ }), the value passed to the response will be available in request.pre and the Hapi response object would be present in request.preResponses.

Moving logic out of handler

Let’s say we have a route where you can fetch a given resource by {id}. The following code fetches a country by country code, as an example:

server.route({
  method: 'GET',
  path: '/countries/{country}',
  async handler(request, h) {
    const {
      payload,
    } = await Wreck.get(
      `https://restcountries.eu/rest/v2/alpha/${request.params.country}`,
      { json: true }
    )
    return payload
  },
})

While we need the request to have access to the country resource, it’s not really part of our “business logic”, is mainly boilerplate. This is a good candidate for moving it to a prerequisite handler.

server.route({
  method: 'GET',
  path: '/countries/{country}',
  handler(request, h) {
    const { country } = request.pre
    return country
  },
  options: {
    pre: [
      {
        assign: 'country',
        async method(request, h) {
          const {
            payload,
          } = await Wreck.get(
            `https://restcountries.eu/rest/v2/alpha/${request.params.country}`,
            { json: true }
          )
          return payload
        },
      },
    ],
  },
})

Now we’re getting the resource in a prior step to executing the handler, and now it’s available in a property in the request.pre.country. What if we wanted to do a “side-effect” as a prerequisite? Meaning that its a prerequisite that doesn’t return a value, perhaps perform a validation of the resource.

Splitting steps even further with sequential steps

Since pre is an array, the next element of the array will have access to prior assigned values. This means that if we add a second prerequisite if will have access to request.pre.country.

server.route({
  method: 'GET',
  path: '/countries/{country}',
  handler(request, h) {
    const { country } = request.pre
    return country
  },
  options: {
    pre: [
      {
        assign: 'country',
        async method(request, h) {
          const {
            payload,
          } = await Wreck.get(
            `https://restcountries.eu/rest/v2/alpha/${request.params.country}`,
            { json: true }
          )
          return payload
        },
      },
      {
        async method(request, h) {
          const { country } = request.pre
          if (!country.name) {
            throw new Error('country missing a name')
          }
        },
      },
    ],
  },
})

In this case, if the country is missing its name, then your handler will not be called and your consumer will get an HTTP 500 error.

Take control of prerequisite error

We could also add a failAction to either log or ignore in the validation, and that would make the handler being called.

server.route({
  method: 'GET',
  path: '/countries/{country}',
  handler(request, h) {
    // country would be a boom error in case of failure
    const { country } = request.pre

    if (country instanceof Boom) {
      // do something with the case the country is an error
    }

    return country
  },
  options: {
    pre: [
      {
        assign: 'country',
        failAction: 'log',
        async method(request, h) {
          const {
            payload,
          } = await Wreck.get(
            `https://restcountries.eu/rest/v2/alpha/${request.params.country}`,
            { json: true }
          )
          payload.name = null
          return payload
        },
      },
    ],
  },
})

What if we generate a response in the prerequisite?

If we return a h.response([value]) from a prerequisite handler, the value as a property in request.pre and the response instance is present in response.preResponses.

server.route({
  method: 'GET',
  path: '/prerequisite/response',
  handler(request, h) {
    /**
     * `pre.response` will have the value: ""
     * `preResponse.response` will have the actual response
     */
    const { redirect } = request.preResponses

    return redirect
  },
  options: {
    pre: [
      {
        assign: 'redirect',
        async method(request, h) {
          return h.redirect('/')
        },
      },
    ],
  },
})

In the example above, request.pre.redirect will have an empty string as a value, and request.preResponses.redirect will have the actual redirect response from the prerequisite. You can return that value and get the consumer to redirect to the expected path.

Handling parallel prerequisites

An interesting thing about prerequisites, is that as an element of the array you can have another array, and that array will be executed concurrently. Let’s take for example the country example, after fetching the country we could do a concurrent fetch of the flag and the currencies.

server.route({
  method: 'GET',
  path: '/countries/{country}',
  handler(request, h) {
    // country would be a boom error in case of failure
    const { country } = request.pre

    if (country instanceof Boom) {
      // do something with the case the country is an error
    }

    const { currencies, flag } = request.pre

    return country
  },
  options: {
    pre: [
      {
        assign: 'country',
        failAction: 'log',
        async method(request, h) {
          const {
            payload,
          } = await Wreck.get(
            `https://restcountries.eu/rest/v2/alpha/${request.params.country}`,
            { json: true }
          )
          return payload
        },
      },
      [
        {
          assign: 'flag',
          async method(request, h) {
            const { flag } = request.pre.country
            const { payload } = await Wreck.get(flag)
            return payload
          },
        },
        {
          assign: 'currencies',
          async method(request, h) {
            const { currencies } = request.pre.country
            return Promise.all(
              currencies.map((currency) =>
                Wreck.get(
                  `https://restcountries.eu/rest/v2/currency/${currency.code}`,
                  { json: true }
                )
              )
            )
          },
        },
      ],
    ],
  },
})

You can see that flag and currencies are within an array inside the pre array, which will make them execute concurrently, and both will have access to the request.pre.country from the previous step.

Conclusion

Routes prerequisites (pre) allow us to keep our handler free of boilerplate related to fetching resources, or doing certain kind of validation or other side effects.

They allow to have a more explicit set of steps, so that when you look at the route configuration you’ll be able to tell what is going on from a quick glance without trying to decipher what is going on in a handler.

Happy coding!


I'm a software architect that enjoys helping people, building platforms, and working in distributed systems at the intersection between people and software.