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_id
CRUD on each if makes sense. For example, /projects POST creates a new project, GET fetches all projects. /projects/project_id GET fetches just that one project.
Items are project specific so I put them under project_id, which is a particular project.
Are there any way of creating this kind of nested routes?
Right now I have something like this:
server.route({
method: 'GET',
path: '/projects',
handler: getAllProjects
});
server.route({
method: 'GET',
path: '/projects/{project_id}',
handler: getOneProject
});
server.route({
method: 'GET',
path: '/projects/{project_id}/items/{item_id}',
handler: getOneItemForProject
});
server.route({
method: 'GET',
path: '/projects/{project_id}/items',
handler: getAllItemsForProject
})
But I am looking for a way to nest items routes into projects routes and for ability to pass project further.
Any recommendations?
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:
// <app>/projects/<projectname>/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
// <app>/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 <project>/index.js
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)
What you're looking for is something similar to Express's Router
. In fact, Express does a good job of burying the usefulness of this feature so I'll re-post an example here:
// routes/users.js:
// Note we are not specifying the '/users' portion of the path here...
const router = express.Router();
// index route
router.get('/', (req, res) => {... });
// item route
router.get('/:id', (req, res) => { ... });
// create route
router.post('/', (req,res) => { ... });
// update route
router.put('/:id', (req,res) => { ... });
// Note also you should be using router.param to consolidate lookup logic:
router.param('id', (req, res, next) => {
const id = req.params.id;
User.findById(id).then( user => {
if ( ! user ) return next(Boom.notFound(`User [${id}] does not exist`));
req.user = user;
next();
}).catch(next);
});
module.exports = router;
Then in your app.js or main routes/index.js where you assemble your routes:
const userRoutes = require('./routes/users')
// now we say to mount those routes at /users! Yay DRY!
server.use('/users', userRoutes)
I'm actually disappointed to find this SO post with no other responses so I'll assume there's nothing out of the box (or even a third-party module!) to achieve this. I imagine it might not be too difficult to create a simple module that uses functional composition to remove duplication. Since each of those hapi route defs is just an object it seems like you could make a similar wrapper like the following (untested):
function mountRoutes(pathPrefix, server, routes) {
// for the sake of argument assume routes is an array and each item is
// what you'd normally pass to hapi's `server.route
routes.forEach( route => {
const path = `${pathPrefix}{route.path}`;
server.route(Object.assign(routes, {path}));
});
}
EDIT In your case since you have multiple layers of nesting, a feature similar to Express's router.param
would also be extremely helpful. I am not terribly familiar with hapi so I don't know if it already has this capability.
EDIT #2 To more directly answer the original question, here is a hapi-route-builder has a setRootPath()
method that lets you achieve something very similar by letting you specify the base portion of the path once.
There isn't great information about such a basic requirement. Currently, I am doing the following & it works well.
Step 1: Contain the routes in a plugin, as so:
// server.js
const server = Hapi.server({ ... })
await server.register(require('./routes/projects'), { routes: { prefix: '/projects' } })
Step 2: Register an ext
within the scope of that plugin.
// routes/projects/index.js
module.exports = {
name: 'projects',
async register(server) {
server.route({
method: 'get',
path: '/', // note: you don't need to prefix with `projects`
async handler(request, h) {
return [ ... ]
}
})
server.route({
method: 'get',
path: '/{projectId}', // note: you don't need to prefix with `projects`
async handler(request, h) {
return { ... }
}
})
server.ext({
// https://hapijs.com/api#request-lifecycle
type: 'onPostAuth',
options: {
// `sandbox: plugin` will scope this ext to this plugin
sandbox: 'plugin'
},
async method (request, h) {
// here you can do things as `pre` steps to all routes, for example:
// verify that the project `id` exists
if(request.params.projectId) {
const project = await getProjectById(request.params.projectId)
if(!project) {
throw Boom.notFound()
}
// Now request.params.project can be available to all sub routes
request.params.project = project
}
return h.continue
}
})
}
}
This has been as close to I have been able to get to recreating Express Router functionality.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With