Express - Conditional Routing - javascript

At my website.com/v2/bridge/:locationId/process endpoint, the incoming req.body looks like this:
{
choice: 'a',
data: [
{
...
},
...
]
}
I want to access a particular route depending on what the value of req.body.choice is. If req.body.choice === 'a' then I want to go on to website.com/v2/bridge/:locationId/process/choiceA with the same req being passed on.
I don't know what middleware I need to use to accomplish that. I don't know if that is even possible.
My extremely simplified routes:
// website.com/v2/bridge
const proc = require('./process');
router.use('/:locationId/process', proc);
module.exports = router;
// website.com/v2/bridge/56/process
router.use(function (req, res, next) {
// ?????????????????????????
next();
});
const choiceA = require('./choice-a');
const choiceB = require('./choice-b');
router.use('/choice-a', choiceA);
router.use('/choice-b', choiceB);
module.exports = router;
// website.com/v2/bridge/56/process/choice-a
router.post('/', function (req, res) {
res.send('I got here.');
return;
});
module.exports = router;
What middleware function do I need to include to conditionally route my request? I am trying to avoid one giant function with if statements that process different things according to the value of req.body.choice.

This will be little trickier for you...give it a try
router.use(function (req, res, next) {
req.path = "/" + "choice-"+req.body.choice
req.url = "/" + "choice-"+req.body.choice
next();
});
now it'will do the request to the end point you want

As part of finding the answer for the same question, I came across with this question, which didn't settle my mind by messing with the req.url, so here's how I got it done (I know it's a long delay, but better late than never):
When you're dealing with routers, and you want to make a condition to decide whether to use, you can do it with two ways (according to expressjs doc), and let's learn them by examples
1. Skipping between routes
function skipThisRouteMiddleware (req, res, next) {
if (true === isAmazingValidation) {
return next('route');
}
return next();
}
router.get('/user/:id',
skipThisRouteMiddleware,
getUserV1 // TBD - your old route
)
router.get('/user/:id',
getUserV2 // TBD - your new route
)
In the case above, when you have two routes, and you want to conditionally pick one of them, it can be done by specifying a middleware that makes the validation for the first route only, and when needed, it triggers next('route') which skip to the next matching route, please note that you must specify METHOD and not generally app.use()
2. Skipping between routers
// routers/index.js
const mainRouter = express.Router();
mainRouter.use(oldRouter);
mainRouter.use(newRouter);
// routers/old.js
const oldRouter = express.Router();
function canUpgradeToNewRouter (req, res, next) {
if (true === isAmazingValidation) {
return next('router'); // now it's 'router' and not 'route'
}
return next();
}
oldRouter.use(canUpgradeToNewRouter);
// routers/new.js
const newRouter = express.Router();
newRouter.use(...);
In this case, you have two different routers, and you want to conditionally pick one of them. for this case, you'll have to create a parent router (mainRouter) and two nested routers (oldRouter, newRouter).
The trick here is that the oldRouter tries to run a middleware validation that tries to "upgrade" the requester to the new shiny router, if the condition is true, it will skip the whole oldRouter, and pass the stick to the parent router mainRouter to continue matching routes to this request (the magic - next('router')), which eventually pick the upcoming newRouter
In both methods, we let the first route to make the logic and choose between itself and the others, that's a different perception (a bit)

Related

Node.js express - get route params without going into the route function for quick analytics tracking

I aim to send an analytics event for each route in my API. As opposed to saving an event with the entire full route that was called, I want to save the base url as the event and the parameters of the route as variables of the event. For example, when saving an analytics event...
Not this:
{
event_title: "API User Event"
category: "domain.com/api/user_routes/route_1/value_of_param_one"
}
But this:
{
event_title: "API User Event"
category: "domain.com/api/user_routes/route_1"
params: {
param_one: "value_of_param_one"
}
}
I'd like to have a global function that gets the parameters from the request variable, however, if you do this on a higher level (not route level)
app.use('/api/user_routes/*', myWrapperFunction)
myWrapperFunction will detect anything after /api/user_routes as parameters. From my experiments, I was only able to successfully detect the actual parameters inside a specific route function. However, that approach requires me to either edit each route function or wrap it like so...
router.get('/route_1/:param_one', (req, res) => Utility.analyticsEvent(userController.routeOneFunction, req, res));
router.get('/route_2/:param_one', (req, res) => Utility.analyticsEvent(userController.routeTwoFunction, req, res));
router.get('/route_3/:param_one', (req, res) => Utility.analyticsEvent(userController.routeThreeFunction, req, res));
Is there a way to detect the actual parameters of the route without actually going into the function itself? Or is this a limitation on express because it won't know the specifics of the route until it finds the first matching option traversing down the routes?
Edit If there is no way to know the parameters before express matches the specific route: is there a function that you can run before executing the route function that will tell you which route will be matched and will specify the parameters?
Welcome all comments!
I think one approach is to write a middleware like below.
// This will get executed before every request. As we'll add this with app.use() with top level middlewares
function customMiddleware (req, res, next) {
let url = req.baseUrl;
// some if else logic to re-route
if( url.includes('/api/user_routes')) {
let urlSplit = url.split();
if( url[urlSplit.length() - 1] == 'param_one' ) {
res.redirect('/api/user_routes/route_1')
}
else if(url[urlSplit.length() - 1] == 'param_tow' ) {
res.redirect('/api/user_routes/route_1')
}
// and so on
} else {
// executes other middleware or go to matching route
next()
}
}
app.use(customMiddleware)
Found a way to do it after the call is made by overwriting the response.json function
app.use(function (req, res, next) {
var json = res.json;
res.json = function (body) {
// Send analytics event before returning response
try {
// Routes that don't need to be tracked with analytics
let notIncludeRoutes = [
"some_not_tracked_route"
];
let route = req.baseUrl + req.route.path;
if(notIncludeRoutes.indexOf(route) === -1) {
// Track route and params
let route_params = res.req.params;
Utility.analyticsEvent(route, route_params);
}
} catch(error) {
// Don't block call if there was an error with analytics, but do log it
console.log("ANALYTICS ERROR: ", error);
}
json.call(this, body);
};
next();
});

Handle all routes with a prefix in separate js file

I have multiple routes in my express application for different prefix. Each prefix's routes are defined in separate files.
const routes = require('./server/routes');
app.use('/api', routes.apirouter);
app.use('/', routes.webrouter);
where './server/routes.js' is:
module.exports.apirouter = require('./api');
module.exports.webrouter = require('./webroutes');
Hence currently, I am handling and defined all routes with /api prefix in 'api.js' and all other routes are defined in 'webroutes.js'
Now similarly I need to define all the routes with prefix 'fetch-' to a separate js file 'fetch.js', hence http://localhost/fetch-one and http://localhost/fetch-two need to be defined in fetch.js
However the following code is not working for /fetch-one:
const routes = require('./server/routes');
app.use('/api', routes.apirouter);
app.use('/', routes.webrouter);
app.use('/fetch-*', routes.fetchrouter);
routes.js:
module.exports.apirouter = require('./api');
module.exports.webrouter = require('./webroutes');
module.exports.fetchrouter = require('./fetch');
fetch.js:
Route defined for /fetch-one and /fetch-two separately in fetch.js
var fetchRouter = require('express').Router();
fetchRouter.get('/fetch-one', function(req, res, next) {
// localhost/fetch-one not passed control here
});
fetchRouter.get('/fetch-two', function(req, res, next) {
// localhost/fetch-two not passed control here
})
module.exports = fetchRouter;
The problem is that once you've done this:
app.use('/fetch-*', routes.fetchrouter);
Then, the /fetch-* part of the path has been removed from the routing for the fetchrouter. So, when you then do:
fetchRouter.get('/fetch-one', ...)
That won't match because /fetch-one has already been removed from the routing path. The URL would have to have been /fetch-xxx/fetch-one for that to match.
The simplest design would be to change your paths so that the URLs are /fetch/one and /fetch/two which is much more in line with how Express routers work. Then you'd go with:
app.use('/fetch', routes.fetchrouter);
And, have routes in that router for
app.get('/one, ...)
app.get('/two, ...)
That's the URL design that lines up the cleanest with the way Express routers work the simplest.
If you're going to stay with the /fetch-one URL design, then another idea would be to let the fetchRouter look at all top level URLs:
app.use('/', fetchRouter);
And, then have it only have routes for the top level routes you want it to look at. Express will then continue look for other routes that match if it doesn't handle things:
app.get('/fetch-one', ...);
app.get('/fetch-two', ...);
You need to make sure there are no greedy top level routers that take all requests and make sure that this router only takes the request that it needs so that other top level routes get a chance to get matched.
If you really want to stay with the /fetch-* design for the router, then you can do a bit of your own routing and URL comparison:
app.use('/fetch-*', routes.fetchrouter);
Then, in the fetchrouter:
app.get("/", function(req, res, next) {
switch(req.baseUrl) {
case "/fetch-one":
// process /fetch-one here
break;
case "/fetch-two":
// process /fetch-two here
break;
default:
next();
}
});
I thought of one other option that uses the Express parameters where you would just use a handler function for the fetch routes instead of a router:
app.use('/fetch-:id', routes.fetchHandler);
Then, in the fetchHandler:
function fetchHandler(req, res, next) {
switch(req.params.id) {
case "one":
// process /fetch-one here
break;
case "two":
// process /fetch-two here
break;
default:
next();
}
});
Instead of a big switch, you can make it table driven too which is probably cleaner if you have a lot of routes:
app.use('/fetch-:id', routes.fetchHandler);
Then, fetchHandler would be an exported function:
const routeTable = {
one: routeOne,
two: routeTwo,
three: routeThree,
....
};
function fetchHandler(req, res, next) {
let fn = routeTable[req.params.id];
if (fn) {
fn(req, res, next);
} else {
next();
}
});
function routeOne(req, res, next) {
...
}
function routeTwo(req, res, next) {
...
}
function routeThree(req, res, next) {
...
}

Expressjs middleware static typed path and path with param conflict

I'm about to implement REST endpoints for authenticated users and non-authenticated users in expressjs. My current understanding of REST has led me to following design pattern:
User resource
Token user:
/users/self GET, PATCH
Non-token user:
/users POST
/users/:user_id GET
Profile image resource
Token user:
/users/self/profile-images POST
/users/self/profile-images/:profile_image_id PUT, DELETE
Non-token user:
/users/:user_id/profile-images GET
I'm struggling to figure out how to use this pattern without having :user_id parameter become self, i.e {user_id: 'self'}. I would want them to act as two isolated path types without interference, one strict and one dynamic. Is this possible? If so, how?
A code example of my current implementation looks like following:
// instPrivateUserRestRoutes.js (token-user)
router.use('/users/self', [
instAccountRestRoutes(restControllers),
instAuthenticationSessionRestRoutes(restControllers),
instPrivateProfileImageRestRoutes(restControllers)
])
// instPublicUserRestRoutes.js (non-token user)
router.use('/users/:user_id', [
instPublicProfileImageRestRoutes(restControllers)
])
// instRestApp.js (mount point for top resources)
router.use(instPrivateUserRestRoutes(restControllers))
router.use(instPublicUserRestRoutes(restControllers))
What you could do it to create a conditional routing middleware. The factory dunction takes as first argument a callback method that determines which router should be use, and as second argument a list of routers, and returns a new middle war that conditionally uses one of the routes.
function conditionalRouting(cond, routes) {
return function (req, res, next) {
try{
// get the index for the router that should be used
var idx = cond(req, res)
// call this router and pass req, res and next to it
routes[idx](req, res, next)
} catch(err) {
// in the case an error occurs in on of the steps "throw" that error
next(err)
}
}
}
You could then use it that way:
app.use(
'/users/:user_id',
conditionalRouting(
(req, _) => req.params.user_id === 'self' ? 0:1,
[routerForSelf, routerForUser]
))
An other way would be to handle this case explicitly with a middle ware that triggers a not found error:
function forceNotFound(req, res, next) {
var error = new Error('resource not found')
error.status = 404
next(error)
}
And add this as your last middleware
router.use('/users/self', [
instAccountRestRoutes(restControllers),
instAuthenticationSessionRestRoutes(restControllers),
instPrivateProfileImageRestRoutes(restControllers),
forceNotFound
])
This way it is clear that express should stop at that point.
This will look different to what the standard Cannot GET /the/path would look like, but you normally don't want to display those default error messages anyway.
If you want to have the same kind of message then you could write:
var parseUrl = require('parseurl')
var encodeUrl = require('encodeurl')
app.use((req, res, next) => {
var msg = 'Cannot ' + req.method + ' ' + encodeUrl(parseUrl.original(req).pathname)
var error = new Error(msg)
error.status = 404
next(error)
})
If you only want to handled numeric ids you could use this:
router.use('/users/:user_id(\\d+)', [
instPublicProfileImageRestRoutes(restControllers)
])
That way instPublicProfileImageRestRoutes would only be called if user_id is numberic.

ExpressJS 4 middleware problems

I'm trying to wrap my head arround the middleware in ExpressJS 4.
If I understood correctly is that middleware are applied in the order of declaration and that you can "bind" them at different level.
Here I'm trying to bind a middleware at router level.
function middleware(req, res, next) {
console.log("middleware");
res.write("middleware");
next();
}
function handler(req, res) {
console.log("OK");
res.status(200).send('OK');
}
const router1 = express.Router();
const router2 = express.Router();
router1.get("/1", handler);
router2.get("/2", handler);
I would except the following to print OK when calling /test/1 and middleware on /test/2.
app.use("/test/", router2.use(middleware), router1);
But the output seems to be inverted and is equivalent to:
app.use("/test/", router2, middleware, router1);
What I really want is that only the first router to use the middleware.
In other word scope the use of the middleware to the first controller.
I could easily swap the order of router1 and router2 but my other requirement is because my router2 use in fact a route that catch all requests (/:id) I need to have it last.
What I'm missing here and how can I do what I want ?
EDIT for clarification:
What I ultimely want is something along this:
/
|-test/
|-route // use middleware
|-something // use middleware
|-another // use middleware
...
|-:id // without middleware
That's why I have a router with many routes that are under router1 where I want the middleware.
And router2 with a catch-all without the middleware.
You can set a middleware when defining the routes:
router1.get("/1", middleware, handler);
router2.get("/2", handler);
The 1st will use the middleware and the second not.
BTW I would suggest the followings: do not create separate router for each route, only one is enough.
const router = express.Router();
router.get("/1", handler);
router.get("/2", handler);
app.use("/test/", router);
As router1 and router2 are bound to a common route, they will be executed in order, along with their middlewares until one of the routers route matches with the requested path/route.
In your case, as you can't swap the order of the routers, you can create a middleware that wraps the router, checks if the requested path/route exists in the router and if so, returns it, otherwise just skip it.
var unlessMatch = function(router) {
let routerPaths = [];
// Retrieve, create a regex and store every route of the Router
router.stack.forEach(layer => {
if (layer.route) {
routerPaths.push(layer.route.path.replace(/\/?(:[^\/]+)(\/?)/g, "/[^\/]+"));
}
});
return function(req, res, next) {
// Check if requested route exists in the router
routerPaths.every(path => {
return new RegExp('^' + path + '(\/)?$').test(req.path) ? router(req, res, next) : true;
});
return next();
};
};
function middleware(req, res, next) {
console.log("middleware");
next();
}
function handler(req, res) {
console.log("OK");
res.status(200).send('OK');
}
const router1 = express.Router();
const router2 = express.Router();
// Bind middleware to router2
router2.use(middleware);
router1.get("/1", handler);
router2.get("/2", handler);
// Wrap router2 into unlessMatch middleware/function so that router2's middlewares
// are not executed if no route matches with the requested one
app.use("/test/", unlessMatch(router2), router1);

node.js & express: for loop and `app.get()` to serve articles

I'm working on a node js express application that uses app.get() to serve the different web addresses. So app.get('/',{}); serves the home page, app.get('/login'{ }); serves the login page, etc.
Is it good practice to programatically serve pages using a for loop, like in this example?
var a = ["a", "b", "c"];
a.forEach(function(leg) {
app.get('/' + leg, function(req, res){
res.render('index.ejs', {
word : leg
});
});
});
index.ejs is just
<p><%= word %> </p>
So that site.com/a, site.com/b, and site.com/c are all webpages.
I want to utilize this to run .foreach() on a list of all of the article titles (coming from a database of stored articles).
EDIT: The website allows users to submit posts, which become "articles" in a database. I want this loop to route to new submitted articles after they've been posted. If I want to do app.get('/' + newPageName); for user submitted pages AFTER I've already started the server with node server.js, how is that achieved?
Make use of middlewares to better handle the requests. I assume you will have tens/hundreds of posts, adding routes for them like what you've done, is not so elegant.
Consider the following code; I am defining routes of /posts/:legId form. We will match all requests to this route, fetch the article and render it.
If there are handful of routes to be defined you could make use of regular expression to define them.
// dummy legs
var legs = {
a: 'iMac',
b: 'iPhone',
c: 'iPad'
};
app.get('/posts/:leg', fetch, render, errors);
function fetch(req, res, next) {
var legId = req.params.leg;
// add code to fetch articles here
var err;
if (!legs[legId]) {
err = new Error('no such article: ' + legId);
}
req.leg = legs[legId];
next(err);
}
function render(req, res, next) {
res.locals.word = req.leg;
res.render('index');
}
function errors(err, req, res, next) {
console.log(err);
res.locals.error = err.message;
// render an error/404 page
res.render('error');
}
Hope this helps, feel free to ask me any questions you may have.
No, you should not be generating route handlers like that. This is what route parameters are for.
When you start a path component (folder) with a : in an Express route, Express will match any URL that follows the pattern automatically, and place the actual value in req.params. For example:
app.get('/posts/:leg', function(req, res, next) {
// This will match any URL of the form /post/something
// -- but NOT /post/something/else
if (/* the value of req.params.leg is valid */) {
res.render('index.ejs', { word: req.params.leg });
} else {
next(); // Since the user requested a post we don't know about, don't do
// anything -- just pass the request off to the next handler
// function. If no handler responds to the request, Express
// defaults to sending a 404.
}
});
In the real world, you'd probably determine if the leg param is valid by doing a database lookup, which entails making an async call:
app.get('/posts/:leg', function(req, res, next) {
db.query(..., req.params.leg, function(err, result) {
if (err) next(err); // Something went wrong with the database, so we pass
// the error up the chain. By default, Express will
// return a 500 to the user.
else {
if (result) res.render('index.ejs', result);
else next();
}
});
});

Categories