最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Hapi nested routing - Stack Overflow

programmeradmin4浏览0评论

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?

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?

Share Improve this question edited Jun 28, 2017 at 10:33 laser 1,37613 silver badges14 bronze badges asked Mar 19, 2015 at 11:18 PoMaHTuKPoMaHTuK 2232 silver badges7 bronze badges 6
  • I don't really understand what your question is. Your wanted nested routes, and you've supplied the working code above. So where are you stuck? – Matt Harrison Commented Mar 19, 2015 at 12:45
  • 1 I am wondering if there any other, more convenient way, because in this approach routing for third level will become a huge mess. server.route({ method: 'GET', path: '/projects/{project_id}/items/{item_id}/results/{result_id}', handler: getAllItemsForProject }) So a im looking for solution, similar to this one: link – PoMaHTuK Commented Mar 19, 2015 at 14:44
  • I think the problem with nesting is that Hapi doesn't really has a way of having multiple handlers per route definition. You still have to explicitly declare each route, and as of right now, Hapi doesn't really has a way of declaring child routes. – Osukaa Commented Mar 23, 2015 at 23:13
  • @PoMaHTuK can you post an example of how you would solve this in another framework or some code for how you would see the API working for this if it did exist in hapi? – Matt Harrison Commented Mar 27, 2015 at 6:17
  • @PoMaHTuK I know this isn't the answer your looking for, but I would highly advice you to flatten the API. True each Project has its Items but as the API becomes more complex this will be very messy, lets say that each item has Watchers. Now your API endpoints will be '/projects/{project_id}/items/{item_id}/watchers' But each watcher has his Projects... When will this stop ? What endpoint will you use to Fetch all Items being watched by a specific User ? How will you share the Item fetching logic between different endpoints ? Flat API using GET params to filter data will be more useful. – Yoni Jah Commented Mar 31, 2015 at 6:40
 |  Show 1 more comment

3 Answers 3

Reset to default 8 +150

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:

  • if the project handling module really are very similar, you should create a library which you use to prevent copying the same module over and over again, as that makes maintenance easier (which, I recon, was the ultimate goal of having sub-routing)
  • if you can figure out which modules to use at runtime, you should also be able to figure this out when the server process starts.

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.

发布评论

评论列表(0)

  1. 暂无评论