ExpressJS 4 middleware problems - javascript

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);

Related

Possible to use router.param() inside a nested router in express.js?

I'm working on a code challenge and trying to challenge myself to make something that works as well as something that is well organized.
I have a router set up and functioning. This router in turn uses an additional router. All is working so far. Now I am trying to run router.param() to preform some logic in the child router using the parameter that comes from the parent router. I have included {mergeParams: true} when creating the child router.
I would like to run additional logic using the parameter for all http methods in the child router, but don't seem to be able to. Can I not use router.param() for this purpose? I also seem to not have access to req.minion in the child router (which was attached using router.param() in the parent).
Alternatively is there a way to carry over logic from the router.param() in the parent router to the child router?
Parent router
const {getFromDatabaseById, getAllFromDatabase} = require('./db');
const express = require('express');
const workRouter = require('./workRouter.js');
const minionsRouter = express.Router();
minionsRouter.use('/:minionID/work', workRouter);
minionsRouter.param('minionId', (req, res, next, minionId) => {
const minion = getFromDatabaseById('minions', minionId);
if(minion){
req.minion = minion;
next();
}else{
res.status(404).send();
}
});
minionsRouter.get('/', (req, res, next) => {
res.send(getAllFromDatabase('minions'));
});
minionsRouter.get('/:minionId', (req, res, next) => {
res.send(req.minion);
});
//Some additional http methods here
module.exports = minionsRouter;
Child router
const {getAllFromDatabase} = require('./db');
const express = require('express');
const workRouter = express.Router({mergeParams: true});
workRouter.param('minionId', (req, res, next, minionId) => {
const allWork = getAllFromDatabase('work');
const minionWork = allWork.filter(work => work.minionId === minionId);
console.log(minionWork);
if(minionWork){
req.minionWork = minionWork;
next();
}else{
res.status(404).send();
}
});
workRouter.get('/', (req, res, next) => {
res.send(req.minionWork);
});
//Some additional http methods here
module.exports = workRouter;
The get methods in the parent router work perfectly, and the get method in the child router works if I move the logic in the router.param() call to it, but as is it does not seem to attach minionWork to req. If router.param() won't work for this, how can I avoid repeating that code for all http methods in the child router? Thanks!

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) {
...
}

chain middleware functions in custom function

I know that I can chain middleware functions after passing in the route like
const express = require('express');
const router = express.Router();
router.post('/', middlewareFunction1, middlewareFunction2, controllerFunction);
module.exports = router;
I would like to know if it's possible to call only one function (called gateway)
router.post('/', gatewayFunction1);
and this function is able to chain all those methods
const controller = require('../controllers/controller');
function gatewayFunction1(request, response, next) {
// validate route
// do other middleware stuff
// call controller.function1
}
module.exports = {
gatewayFunction1,
};
Why would I do that? I was thinking about separating the middleware logic from the routes. This gateway should just get executed after routing and before calling the router.
I tried to return an array of functions (example code)
function gatewayFunction1(request, response, next) {
return [(request, response, next) => {
console.log('called middleware 1');
next();
}, (request, response, next) => {
console.log('called middleware 2');
next();
}, (request, response, next) => {
response.status(200).json({
message: 'that worked',
});
}];
}
but when I call this api route I get no response
Could not get any response
so it keeps loading forever. Is there a way to chain these middleware functions within another function?
Your gatewayFunction1 does nothing except returns an array.
Just use router.
const express = require('express');
const gatewayFunction1 = express.Router();
gatewayFunction1.use(middlewareFunction1, middlewareFunction2, controllerFunction);
module.exports = gatewayFunction1;
Then
const gatewayFunction1 = require('...'); // where you define gatewayFunction1
router.post('/', gatewayFunction1);
Middleware should be a function and you are returning an array.If next function is not called it will get stuck. I don't like the whole idea combining them but I think the best way is to import all your middleware functions in one function and call them individually then use that function as your combined middleware.

How to trigger next() in express Router.route and controller pattern?

I can't seem to figure out how to implement 'next()' after the request lands to one of my api routes. I want to do logging after everything is done, and not before my api routes. I have implemented the controller pattern on my routing. I can only do logging for now on errors, but I want to do logging on all requests:
//------ api/index.js
import routes from './routes/index.route';
app.use('/api', routes);
// ??? HOW TO HANDLE next() called in "/api routes"
// only handles incoming requests not found in "/api routes"
app.use((req, res, next) => {
// logic here...
next(error);
});
// global error handler
app.use((error, req, res, next) => {
// logging logic here...
});
//------ routes/index.route.js
import express from 'express';
import userRoutes from './user.route';
import bookRoutes from './book.route';
const router = express.Router();
router.use('/user', userRoutes);
router.use('/book', bookRoutes);
//------ routes/book.route.js
import express from 'express';
import bookCtrl from './../controllers/book.controller';
const router = express.Router();
router.route('/:bookId').get(bookCtrl.getBook);
//------ controllers/book.controller.js
export const getBook = async (req, res, next) => {
const book = // get book logic here;
return res.status(httpStatus.OK).json(book);
// HOW DO I TRIGGER next()?
// I tried this approach as well:
// res.status(httpStatus.OK).json(book);
// next();
//however, it would say, "Error: Can't set headers after they are sent."
}
Thank you so much.
I figured out a way to trigger next which is not sending a response immediately instead assigning it to res.locals and calling next. Here are the changes based on my above code:
//------ api/index.js
// handle next() event coming from "/api routes"
// and also handles incoming requests not found in "/api routes"
app.use((req, res, next) => {
if (res.locals.status) {
const { status, responseData } = res.locals;
res.status(status).json(responseData);
// logging logic here...
} else {
// error logic here...
// forward this error into the global error handler
next(error);
}
});
// controllers/book.controller.js
export const getBook = async (req, res, next) => {
// get book logic here..
const book = // get book logic here;
res.locals = {
status: httpStatus.OK,
responseData: {
book,
},
};
next();
}
Thanks to #destoryer for the res.json tip! :)
Just replace
return res.status(httpStatus.OK).json(book);
// HOW DO I TRIGGER next()?
with
res.status(httpStatus.OK).json(book);
next();
And it will work.

Express - Conditional Routing

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)

Categories