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
handlers) configurations?
Prerequisites accomplish different goals, but they’re meant 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
assign
method
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:
request.pre
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
handlers) 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!