Suppose I want to have REST endpoints which look roughly like this:
/projects/
/projects/project_id
/projects/project_id/items/
/projects/project_id/items/item
While there is no concept of "subrouting" (that I know of) in hapi itself, the basics are easy enough to implement.
First off, hapi offers wildcard variables in paths, using these you basically create a catch-all route for a given path. For example:
server.route({
method: 'GET',
path: '/projects/{project*}',
handler: (request, reply) => {
reply('in /projects, re-dispatch ' + request.params.project);
}
});
There are some rules to these wildcard paths, the most important one being it can only be in the last segment, which makes sense if you think of it as a "catch-all".
In the example above, the {project*}
parameter will be available as request.params.project
and will contain the remainder of the called path, e.g. GET /projects/some/awesome/thing
will set request.params.project
to some/awesome/project
.
The next step is to handle this "sub-path" (your actual question), which is mostly a matter of taste and how you would like to work. Your question seems to imply you don't want to create an endless repeating list of pretty much similar things but at the same time to be able to have very specific project routes.
One way would be to split up the request.params.project
parameter into chunks and look for folders with matching names, which could contain logic to handle the request further.
Let's explore this concept by assuming a folder structure (relative to the file containing the route, e.g. index.js
) which can be easily be used to include the handlers for the specific routes.
const fs = require('fs'); // require the built-in fs (filesystem) module
server.route({
method: 'GET',
path: '/projects/{project*}',
handler: (request, reply) => {
const segment = 'project' in request.params ? request.params.project.split('/') : [];
const name = segment.length ? segment.shift() : null;
if (!name) {
// given the samples in the question, this should provide a list of all projects,
// which would be easily be done with fs.readdir or glob.
return reply('getAllProjects');
}
let projectHandler = [__dirname, 'projects', name, 'index.js'].join('/');
fs.stat(projectHandler, (error, stat) => {
if (error) {
return reply('Not found').code(404);
}
if (!stat.isFile()) {
return reply(projectHandler + ' is not a file..').code(500);
}
const module = require(projectHandler);
module(segment, request, reply);
});
}
});
A mechanism like this would allow you have each project as a node module in your application and have your code figure out the appropriate module to use to handle the path at runtime.
You don't even have to specify this for every request method, as you can simply have routes handle multiple methods by using method: ['GET', 'POST', 'PUT', 'DELETE']
instead of method: 'GET'
.
It does not, however, entirely deal with the repetitive declaration of how to handle routes, as you will need a rather similar module setup for every project.
In the way the example above includes and invokes "sub-route-handlers", a sample implementation would be:
// /projects//index.js
module.exports = (segments, request, reply) => {
// segments contains the remainder of the called project path
// e.g. /projects/some/awesome/project
// would become ['some', 'awesome', 'project'] inside the hapi route itself
// which in turn removes the first part (the project: 'some'), which is were we are now
// /projects/some/index.js
// leaving the remainder to be ['awesome', 'project']
// request and reply are the very same ones the hapi route has received
const action = segments.length ? segments.shift() : null;
const item = segments.length ? segments.shift() : null;
// if an action was specified, handle it.
if (action) {
// if an item was specified, handle it.
if (item) {
return reply('getOneItemForProject:' + item);
}
// if action is 'items', the reply will become: getAllItemsForProject
// given the example, the reply becomes: getAllAwesomeForProject
return reply('getAll' + action[0].toUpperCase() + action.substring(1) + 'ForProject');
}
// no specific action, so reply with the entire project
reply('getOneProject');
};
I think this illustrates how individual projects can be handled within you application at runtime, though it does raise a couple of concerns you will want to deal with when building your application architecture:
Creating a library to prevent repetitive code is something you should do (learn to do) early on, as this make maintenance easier and your future self will be thankful.
Figuring out which modules will be available to handle the various projects you have at the start of the application will save every request from having to apply the same logic over and over again. Hapi may be able to cache this for you, in which case it doesn't really matter, but if caching is not an option you might be better off using less dynamic paths (which - I believe - is the primary reason this is not offered by hapi by default).
You can traverse the projects folder looking for all
at the start of the application and register a more specific route using glob like this:
const glob = require('glob');
glob('projects/*', (error, projects) => {
projects.forEach((project) => {
const name = project.replace('projects/', '');
const module = require(project);
server.route({
method: 'GET',
path: '/projects/' + name + '/{remainder*}',
handler: (request, reply) => {
const segment = 'remainder' in request.params ? request.params.remainder.split('/') : [];
module(segment, request, reply);
}
});
});
});
This effectively replaces the above logic of looking up module on every request and switch to a (slightly) more efficient routing as you are talling hapi exactly which projects you'll be serving while still leaving the actual handling to every project-module you provide.
(Don't forget to implement the /projects
route, as this now needs to be done explicitly)