Stephen Last
Stephen Last

Reputation: 5781

Express routes for API - URL handlers when you have sub resources

I have two resources, employees and employee groups. I'm trying to implement a nice URL structure like:

Using ExpressJS I have:

router.get('/employees', (req, res, next) => { next(); });
router.get('/employees/:id', (req, res, next) => { next(); });
router.get('/employees/groups', (req, res, next) => { next(); });
router.get('/employees/groups/:id', (req, res, next) => { next(); });
router.all('*', (req, res) => { res.send('...'); });

This doesn't work, because Express can't tell the difference between /employees/:id and /employees/groups. It thinks groups is an id because /employees/:id comes first.

I did have URL's like:

Which works, but doesn't have the nice resource/sub-resource format. The groups are groups of employees and so I'd like the URL's to match that.

If I were getting the groups for an employee it would be fine (/employees/:id/groups), but I'm getting all groups, which are employee groups.

How could I set up Express routes to route properly while still keeping the URL structure I want..?

I guess I need a way for Express to distinguish between an id and a sub-resource. Is there any way to do that..?

UPDATE

I obviously should've said that I'm using next() in each handler, because I need Express to move onto another middleware function, one that controls the response of all requests. It's this other middleware function that actually sends a response. So I need:

  1. Handler for the route.
  2. Handler for all requests.

Upvotes: 1

Views: 1498

Answers (2)

Stephen Last
Stephen Last

Reputation: 5781

RobbyD set me on the right track. This is what I've ended up with:

index.js

router.all('*', setupHandler);
router.get('/employees', getEmployees);
router.get('/employees/groups', getGroups);
router.get('/employees/groups/:id', getGroup);
router.get('/employees/:id', getEmployee);
router.use(errorHandler);

setupHandler()

function setupHandler(req, res, next) {
    res.locals.standardRes = {
        "some": "data"
    };
    res.locals.doResponse = (res) => {
        // ...
        res.json(res.locals.standardRes);
    };
    next();
}

getEmployees()

function getEmployees(req, res, next) {

    somethingThatReturnsAPromise().then(data => {
        // add to res.locals.standardRes here
        res.locals.doResponse(res);
    }).catch(err => {
        next(err);
    });

}

errorHandler()

function errorHandler(err, req, res, next) {
    console.log('err', err);
    // add to res.locals.standardRes here
    // set correct res.status here
    res.locals.doResponse(res);
}

So the handlers are in the order in RobbyD's answer. I've used res.locals to hold a response function (doResponse(res)) to call from each handler. If there's an error I call next(err) as normal to move to errorHandler().

I guess it's all about getting the right flow from middleware to middleware and sending the response at the right time.

Upvotes: 0

RobbyD
RobbyD

Reputation: 513

Express searches for the first route that matches and handles it with the provided function.

Try the other way around:

router.get('/employees', (req, res) => {});
router.get('/employees/groups', (req, res) => {});
router.get('/employees/groups/:id', (req, res) => {});
router.get('/employees/:id', (req, res) => {});

Now express will work its way trough the routes, '/employees/123' will only match on the last route, so that one will be used by express. '/employees/groups' will be matched sooner by the second route and that one will be used.

Very simple but these things can cost you some time figuring out.

Upvotes: 4

Related Questions