hapi-route-builder
v0.3.0
Published
A fluid/builder pattern approach to building Hapi routes.
Downloads
10
Readme
Overview
hapi-route-builder is a library for building routes in Hapi. The goal of this library is to allow for dynamic and terse route building while isolating an application's routes from possible breakages when updating Hapi versions.
hapi-route-builder uses the Builder Pattern to create route configuration. The library's fluent API makes route configuration readable.
To help reduce duplication in the configuration the library includes the ability to set default configuration and to constrain those defaults to certain routes or route patterns.
Example
Before
server.route({
method: "POST",
path: "/foo/{foo_id}/bar/{bar_id}/",
handler: handle,
config: {
validate: {
payload: {
keywords: validateKeywords
}
},
pre: [
[
{
assign: "userData",
method: shared.loadUserData
},
{
assign: "contextData",
method: shared.loadFooData
}
],
shared.authorizeForCreate
]
}
});
Here is a simple POST route which creates a bar
resource. It includes some validation for some payload data. It also includes pre
processing which retrieves, in parallel, user data and some information about the parent foo
resource before ensuring the user has the ability to create the given resource.
After
var config = new RouteBuilder()
.post("/foo/{foo_id}/bar/{bar_id}/", handle)
.validatePayloadKey("keywords", validateKeywords)
.preParallel(["userData", shared.loadUserData], ["contextData", shared.loadFooData])
.preSerial(shared.authorizeForCreate)
.build()
server.route(config);
This cuts 24 lines down to 7, while making some of what Hapi is doing more clear, rather than implied via the object structure. For instance, preParallel
, which takes n Arrays as input, executes each of the items it is passed in parallel.
With Defaults
Using the example above, imagine that for every call to /foo/{foo_id}/bar
user and context data must be loaded into the request before making a judgement regarding whether or not the user is allowed to access the bar
resource. Using defaults that behavior can be defined across all the /foo/{foo_id}/bar
routes using the same pre
config.
var def = new RBDefault(function(rb) {
rb.preParallel(["userData", shared.loadUserData], ["contextData", shared.loadFooData])
.preSerial(shared.authorizeForCreate)
})
.applyAtBuild()
.only("/foo/{foo_id}/bar/");
RouteBuilder.addDefault(def);
Now the route config for all the /foo/{foo_id}/bar
routes need not create that configuration.
var config =
new RouteBuilder()
.post("/foo/{foo_id}/bar/", handle)
.validatePayloadKey("keywords", validateKeywords)
.build()
server.route(config);
But the output is the same.
Install and Usage
npm install --save hapi-route-builder
Usage
var hrb = require("hapi-route-builder");
var RouteBuilder = hrb.RouteBuilder;
var RBDefault = hrb.RBDefault;
Route Building
Use the RouteBuilder
by instantiating a new instance and then chaining its functions. Calling the build
function will return the Hapi route configuration object.
var config =
new RouteBuilder()
.post()
.path("/api/foo")
.build();
Route Defaults
To utilize default configuration across routes instantiate an RBDefault
. It takes as part of its constructor a function that takes a RouteBuilder
instance. Any of the RouteBuilder
functions can then be chained.
Add the RBDefault
instance to the RouteBuilder staticly using RouteBuilder.addDefault
.
var def = new RBDefault(function(rb) {
rb.preParallel(["userData", shared.loadUserData], ["contextData", shared.loadFooData])
.preSerial(shared.authorizeForCreate)
})
.applyAtBuild()
.only("/foo/{foo_id}/bar");
RouteBuilder.addDefault(def);
When defaults are applied
A RBDefault
can either be applied when RouteBuilder
is constructed or when build
is called. By default it is applied when RouteBuilder
is constructed. This sets up defaults before any other configuration is applied. This means a default can be overridden by future calls.
var def = new RBDefault(function(rb) {
rb.path("/api/foo")
});
RouteBuilder.addDefault(def);
new RouteBuilder()
.path("/api/bar")
.build();
This applies the default before path
is called. path
overrides the default so that the output contains path:"/api/bar"
.
RBDefault
s can also be applied when build
is called. This is best used when default overriding is not needed. A default will be applied at build
when the applyAtBuild
function is called on the RBDefault object.
var def = new RBDefault(function(rb) {
rb.path("/api/foo")
}.applyAtBuild());
RouteBuilder.addDefault(def);
new RouteBuilder()
.path("/api/bar")
.build();
In this case the default overrides the configuration and path
would be set to /api/foo
.
Configuration Replacements & Forced Replacements
When build
is called on a route, the RouteBuilder performs several tasks upon the assembled configuration.
First it applies defaults that have indicated they need to be applied duringt he build.
Next it will address any replacements registered for the routes using the replace
function. This allows for default config to be overridden, or possibly any external configuration to be used to update route config.
Last it will check the entire configuration to see if any values, either object values or values of Arrays, are set to a string that both begins and ends with %
. If any such string exists anywhere in the configuration, an Error will be thrown. Strings surrounded with percent signs must be replaced using the replace
function. This forced replacement functionality allows a default to be applied that must be implemented by all, or a subset, of routes.
As a naive example, assume all routes must have a path
.
RouteBuilder.addDefault(new RBDefault(function(rb) {
rb.path("%replaceme%")
}));
Now, when build
ing a route, if the path
hasn't been set, build
will throw an error.
new RouteBuilder()
.post()
.build() // throws error because path not set.
To replace it, use the replace
function.
new RouteBuilder()
.post()
.replace("%replaceme%", "/api/foo")
.build() // does not throw error
API
RouteBuilder
For details about the config being set up by the RouteBuilder, check the hapi route documentation for route configuration and additional route options.
constructor
The RouteBuilder constructor is the first call that begins the fluid API chain.
new RouteBuilder()
RouteBuilder.addDefault
This static function adds defaults to all RouteBuilder instances. This function takes a RBDefault
object as input. If something other than an RBDefault is passed in, an Error will be thrown.
RouteBuilder.addDefault(new RBDefault(function(rb){
rb.post(); // this would make all routes POSTs, here as short example only
}));
RouteBuilder.clearDefaults
This static function clears all defaults out of the RouteBuilder so future routes do not contain any configured defaults. Important to note that this does not effect any routes created prior to clearing the defaults.
This function is handy for clearing defaults after creating a group of routes, prior to adding new defaults for another group.
RouteBuilder.clearDefaults();
RouteBuilder.setRootPath
This static function sets a root path for all paths created after setRootPath
is called. So if path('/profile')
is called on a RouteBuilder after setRootPath('/api')
is called, then the resulting route path would be /api/profile
.
To clear the root path, simply call setRootPath
with no parameters.
RouteBuilder.setRootPath("/api");
... build a route
RouteBuilder.setRootPath();
app
Sets the config.app
property.
var appConfig = {
foo:"bar",
baz: false
};
new RouteBuilder().app(appConfig).build();
build
Creates a configuration object and returns it. When build
is called is also when any defaults that are to be applied during build are applied.
new RouteBuilder()
.post()
.path("/api/foo")
.handler(function(request, reply) {
reply("foo");
})
.build();
cache
Configures the config.cache
parameter. cache
can take 3 different parameter types.
- It can take the full
cache
configuration object as a parameter.
new RouteBuilder().cache({
privacy: "private",
expiresIn: 1000 * 60 * 60
});
- It can take a string in the form
HH:MM
, which sets the cacheprivacy
todefault
andexpiresAt
to the value provided.
new RouteBuilder().cache("12:00");
- It can take a number of milliseconds until the cache is invalidated. The
expiresIn
setting. This also uses thedefault
privacy
.
new RouteBuilder().cache(1000 * 60 * 60);
cachePrivate
Configures cache with the privacy
setting set to private
. Takes one parameter, either expiresIn
or expiresAt
. expiresIn
is a number expressed in millis, expiresAt
is a string in HH:MM
format.
new RouteBuilder().cachePrivate(1000 * 60 * 60);
new RouteBuilder().cachePrivate("12:00");
cachePublic
Configures cache with the privacy
setting set to public
. Takes one parameter, either expiresIn
or expiresAt
. expiresIn
is a number expressed in millis, expiresAt
is a string in HH:MM
format.
new RouteBuilder().cachePublic(1000 * 60 * 60);
new RouteBuilder().cachePublic("12:00");
delete
Expresses a route is a DELETE.
new RouteBuilder().delete()
The delete
function can also take the path
and a handler
as 1st and 2nd arguments. Both arguments do not need to be provided.
new RouteBuilder()
.delete("/api/foo", function(request, reply) {
reply("foo")
})
get
Expresses a route is a GET.
new RouteBuilder().get()
The get
function can also take the path
and a handler
as 1st and 2nd arguments. Both arguments do not need to be provided.
new RouteBuilder()
.get("/api/foo", function(request, reply) {
reply("foo")
})
handler
Sets the handler
property for a Hapi route.
new RouteBuilder().handler(function(request, reply) {
reply();
});
method
This function configures a Hapi route's method
parameter. It takes a string, normally one of 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', or 'OPTIONS'. The convenience functions below are terser ways to express an HTTP method.
new RouteBuilder().method('POST')
options
Expresses a route is an OPTIONS.
new RouteBuilder().options()
The options
function can also take the path
and a handler
as 1st and 2nd arguments. Both arguments do not need to be provided.
new RouteBuilder()
.options("/api/foo", function(request, reply) {
reply("foo")
})
patch
Expresses a route is a PATCH.
new RouteBuilder().patch()
The patch
function can also take the path
and a handler
as 1st and 2nd arguments. Both arguments do not need to be provided.
new RouteBuilder()
.patch("/api/foo", function(request, reply) {
reply("foo")
})
path
Sets the path
property for a Hapi route.
new RouteBuilder().path("/foo/{foo_id}/bar/{bar_id}/")
post
Expresses a route is a POST.
new RouteBuilder().post()
The post
function can also take the path
and a handler
as 1st and 2nd arguments. Both arguments do not need to be provided.
new RouteBuilder()
.post("/api/foo", function(request, reply) {
reply("foo")
})
pre
Sets the entire pre
config.
var pre = [
[
{
assign: "foo",
method: fooFunc
},
{
assign: "bar",
method: barFunc
}
],
bazFunction
]
new RouteBuilder().pre(pre);
preParallel
preParallel
works like preSerial
except it takes multiple arrays of preSerial
inputs. Each array represents a pre
to execute in parallel with the other arrays of inputs.
new RouteBuilder()
.preParallel(
["foo", fooFunc],
["bar", barFunc]
)
.preSerial("baz", bazFunc)
The above would execute fooFunc
and barFunc
in parallel, and after both are finished, execute bazFunc
.
All the preSerial
input variations are honored, including using an index
integer.
new RouteBuilder()
.preSerial("baz", bazFunc)
.preParallel(
0,
["foo", fooFunc],
["bar", barFunc]
)
preSerial
Hapi's pre
configuration is an array. preSerial
pushes a new entry to that array. preSerial
can be called multiple times, adding more entries to the pre
array.
preSerial
can take all of the 3 types of inputs that Hapi's pre
config can take: 1) an object with keys of assign
, method
, and failAction
, 2) a function (same as just providing a method
and 3) a string which invokes a server.method
.
new RouteBuilder()
.preSerial({assign:"foo", method:fooFunc})
.preSerial(barFunc)
.preSerial("serverMethod(params.id)")
preSerial
has two alternative convenience signatures. preSerial
called be called with two parameters for assign
and method
, and with three paramters for assign
, method
, and failAction
.
new RouteBuilder()
.preSerial("foo", fooFunc)
.preSerial("bar", barFunc, "ignore")
All argument signatures have a variation that takes an index
integer as the first parameter. This allows for placing the preSerial
to be placed into a specific position in the pre
array rather than pushed to the end.
new RouteBuilder()
.preSerial("foo", fooFunc)
.preSerial(0, "bar", barFunc, "ignore")
Add 0
in the example above would, in this case, reverse the order of the two preSerial
s.
put
Expresses a route is a PUT.
new RouteBuilder().put()
The put
function can also take the path
and a handler
as 1st and 2nd arguments. Both arguments do not need to be provided.
new RouteBuilder()
.put("/api/foo", function(request, reply) {
reply("foo")
})
replace
This function will replace specified portions of the config with the provided data when build
is called. The replacement is the last thing that happens before the configuration is returned.
new RouteBuilder()
.pre([{assign:"__placeholder", method:aFunction}])
.replace("__placeholder", "formData");
The above will replace __placeholder
in the created configuration with formData
.
replace
will work with any type of value, including things in arrays. It will not work with object keys, though.
{
path:"/foo/{id}",
pre: [
{
assign:"foo",
method:bar
}
]
}
So if the above was configuration that would be generated by build
, if the following replace
functions were called...
.replace("/foo/{id}", "foo/{foo_id}")
.replace({
assign:"foo",
method:bar
}, bar)
...the generated config would become
{
method:"POST",
path:"/foo/{foo_id}",
pre: [ bar ]
}
Notice that replace
can be chained like the other chainable functions.
validatePayload
Sets the config.validate.payload
property of a route configuration.
var validate = {
ids: Joi.array().items(
Joi.string().required()
).required()
};
new RouteBuilder().validatePayload(validate);
validatePayloadKey
A convenience function to set a specific config.validate.payload
key.
var validate = Joi.array().items(
Joi.string().required()
).required();
new RouteBuilder().validatePayload("ids", validate);
vhost
Sets the vhost
property for a Hapi route.
new RouteBuilder().vhost("foo")
RBDefault
constructor
The RBDefault
constructor takes a function as a parameter. The function should take an instance of RouteBuilder
as input. Inside the function the RouteBuilder instance should be used to apply configuration to be used across all routes.
The RBDefault
instance is then passed to RouteBuilder.addDefault
.
RouteBuilder.addDefault(
new RBDefault(function(rb) {
rb.preSerial("auth", authFunction)
});
);
By default, RBDefault
s are applied when a RouteBuilder
is instantiated.
applyAtBuild
This function takes no parameters and indicates that a given RBDefault
is to be applied when the configuration is built rather than when RouteBuilder
is instantiated.
RouteBuilder.addDefault(
new RBDefault(function(rb) {
rb.preSerial("auth", authFunction)
})
.applyAtBuild();
);
only
only
can be chained onto a RBDefault
instance only if applyAtBuild
has first been called. only
allows for a default to be scoped to certain routes. only
can take a RegExp or a string, each of which is used to match any routes the RBDefault may be applied to.
only
itself can be chained.
RouteBuilder.addDefault(
new RBDefault(function(rb) {
rb.preSerial("auth", authFunction)
})
.applyAtBuild()
.only(/account/)
.only("/logout");
);
only
cannot be used with not
.
not
not
can be chained onto a RBDefault
instance only if applyAtBuild
has first been called. not
and allows for specific routes to be eliminated from having the default applied. not
can take a RegExp or a string, each of which is used to eliminate routes from having the RBDefault applied.
not
itself can be chained.
RouteBuilder.addDefault(
new RBDefault(function(rb) {
rb.preSerial("auth", authFunction)
})
.applyAtBuild()
.not(/profile/)
.not("/login");
);
not
cannot be used with only
.