Single express route to capture all possible params combinations - node.js

I want my API to support filtering on the different properties of my mongodb model. The brute force way of which I would use:
app.get('/api/thing/:id', thing.getThingById);
app.get('/api/thing/:name, thing.getThingByName);
app.get('/api/thing/:name/:color', thing.getThingByNameAndColor);
etc. This approach is obviously terrible. How can I add a single route to capture multiple params so that I can return things using something like
exports.getThingByParams = function (req, res, next) {
var query = thingModel.find (req.params);
query.exec (function (err, things) {
if (err) return next (err);
res.send ({
status: "200",
responseType: "array",
response: things
});
});
};

Use the URL query string. It's a set of name/value pairs invented for precisely this use case. It still works great after all these years despite current trends the everything must be in the path portion of the URL instead of the query string because ???. Look at your route - it even says "API" in it. It doesn't need to abuse the path to be "pretty" according to hipsters.
app.get('/api/thing', thing.search);
exports.search = function (req, res, next) {
//Remember any ID values need to be converted from strings to ObjectIDs,
//and there's probably additional sanitization/normalization to do here
var query = thingModel.find (req.query);
query.exec (function (err, things) {
if (err) return next (err);
res.send ({
status: "200",
responseType: "array",
response: things
});
});
};
Then the url to find a red thing named candy would look like
/api/thing?color=red&name=candy

Yup, you can. Just use the most explicit route example:
app.get('/api/thing/:name/:color', thing.getThingByProperty);
and inside getThingByProperty simply test for req.params.YourParam (name or color) and decide what to do. If color is requested but not name, you send in the url as follows:
/api/thing/-/red.
Very common sighting to have null params in routes expressed as dash.
OR, different approach, better pattern more API like (RESTFul) is:
/api/thing/:id that's by id and to offer pattern support
and for different property search use:
/api/things/search?property1=value&property2=value
That way you respect the collection pattern in your API (/api/things) and put the search in the query to keep it flexible. Test inside the search callback for req.query.property1 or property and act accordingly

Related

NodeJS - Wrong Express route being triggered

I have the following routes available for a backend...
admin.js
router.get('/contents', ...); // GET /admin/contents
router.get('/:adminID', ...); // GET /admin/[adminID]
router.put('/:adminID', ...); // PUT /admin/[adminID]
router.get('/', ...); // GET /admin
router.post('/', ...); // POST /admin
.. but in testing, the following:
PUT /admin/contents
triggers the PUT /admin/[adminID] route. But "contents" is not an ID. I understand why this is happening (i.e. it fits into the pattern), but I'm not sure what the best/common solution is to this? Ideally, I'd like it to recognize that "contents" is not an ID, and is in fact just attempting to use an unavailable endpoint.
I could use something like...
router.use('/contents', require('./admin-contents'));
but I'd prefer to limit each top-level endpoint to a single file, opposed to spreading it across so many.
Worst-case scenario, it will look for an admin with ID: "contents", and return "admin not found", but I'd prefer it to return 404, because that is not an available endpoint for /admin.
Edit #1
To clarify, adminID is a mix of letters and numbers, with either occurring in any position in the string. A regex will not work.
Also, the only route for /admin/contents is GET. Having to implement blank routes for all the other methods (PUT, PATCH, DELETE, etc) is not ideal either.
You can provide a regex after a paramenter name in the route, to avoid that scenario.
router.put('/:adminID(\\d+)', (req, res) => {
console.log(req.params.adminID); // I'm a number
});
Now adminID must be a number, otherwise it won't enter the route.
While that's not directly documented on express routing, since express uses path-to-regexp we can see their documentation for this:
And it's documented in Custom Matching Parameters
const regexpNumbers = pathToRegexp('/icon-:foo(\\d+).png')
// keys = [{ name: 'foo', ... }]
regexpNumbers.exec('/icon-123.png')
//=> ['/icon-123.png', '123']
regexpNumbers.exec('/icon-abc.png')
//=> null
UPDATE
Your suggestion of checking for even just one number in a known-length
string should work,
app.put('/:adminID((?:\\w+(?<=\\d+)(?:\\w+)?))', (req, res) => {
// I have at least 1 number
// I can have or not alpha-numeric characters
res.send(req.params.adminID);
});
The regex uses Postive lookbehind assertions which are supported without any flag since Node.js 9.11.2. So if you're using an older version, either upgrade or use the --harmony flag to run it.
You can take advantage of the fact that node interprets handlers in order:
app.put('/admin/contents', (req, res) => res.send('contents'))
app.put('/admin/:adminId', (req, res) => res.send('id'))
When you enter admin/contents, contents is returned, for any other url admin/whatever id is returned.

Call Express router manually

Нello! I am looking to call a function which has been passed to an expressRouter.post(...) call.
This expressRouter.post(...) call is occurring in a file which I am unable to modify. The code has already been distributed to many clients and there is no procedure for me to modify their versions of the file. While I have no ability to update this file for remote clients, other developers are able to. I therefore face the issue of this POST endpoint's behaviour changing in the future.
I am also dealing with performance concerns. This POST endpoint expects req.body to be a parsed JSON object, and that JSON object can be excessively large.
My goal is to write a GET endpoint which internally activates this POST endpoint. The GET endpoint will need to call the POST endpoint with a very large JSON value, which has had URL query params inserted into it. The GET's functionality should always mirror the POST's functionality, including if the POST's functionality is updated in the future. For this reason I cannot copy/paste the POST's logic. Note also that the JSON format will never change.
I understand that the issue of calling an expressjs endpoint internally has conventionally been solved by either 1) extracting the router function into an accessible scope, or 2) generating an HTTP request to localhost.
Unfortunately in my case neither of these options are viable:
I can't move the function into an accessible scope as I can't modify the source, nor can I copy-paste the function as the original version may change
Avoiding the HTTP request is a high priority due to performance considerations. The HTTP request will require serializing+deserializing an excessively large JSON body, re-visiting a number of authentication middlewares (which require waiting for further HTTP requests + database queries to complete), etc
Here is my (contrived) POST endpoint:
expressRouter.post('/my/post/endpoint', (req, res) => {
if (!req.body.hasOwnProperty('val'))
return res.status(400).send('Missing "val"');
return res.status(200).send(`Your val: ${req.body.val}`);
});
If I make a POST request to localhost:<port>/my/post/endpoint I get the expected error or response based on whether I included "val" in the JSON body.
Now, I want to have exactly the same functionality available, but via GET, and with "val" supplied in the URL instead of in any JSON body. I have attempted the following:
expressRouter.get('/my/get/endpoint/:val', (req, res) => {
// Make it seem as if "val" occurred inside the JSON body
let fakeReq = {
body: {
val: req.params.val
}
};
// Now call the POST endpoint
// Pass the fake request, and the real response
// This should enable the POST endpoint to write data to the
// response, and it will seem like THIS endpoint wrote to the
// response.
manuallyCallExpressEndpoint(expressRouter, 'POST', '/my/post/endpoint', fakeReq, res);
});
Unfortunately I don't know how to implement manuallyCallExpressEndpoint.
Is there a solution to this problem which excludes both extracting the function into an accessible scope, and generating an HTTP request?
This seems possible, but it may make more sense to modify req and pass it, rather than create a whole new fakeReq object. The thing which enables this looks to be the router.handle(req, res, next) function. I'm not sure this is the smartest way to go about this, but it will certainly avoid the large overhead of a separate http request!
app.get('/my/get/endpoint/:val', (req, res) => {
// Modify `req`, don't create a whole new `fakeReq`
req.body = {
val: req.params.val
};
manuallyCallExpressEndpoint(app, 'POST', '/my/post/endpoint', req, res);
});
let manuallyCallExpressEndpoint = (router, method, url, req, res) => {
req.method = method;
req.url = url;
router.handle(req, res, () => {});
};
How about a simple middleware?
function checkVal(req, res, next) {
const val = req.params.val || req.body.val
if (!val) {
return res.status(400).send('Missing "val"');
}
return res.status(200).send(`Your val: ${val}`);
}
app.get('/my/get/endpoint/:val', checkVal)
app.post('/my/post/endpoint', checkVal)
This code isn't tested but gives you rough idea on how you can have the same code run in both places.
The checkVal function serves as a Express handler, with request, response and next. It checks for params first then the body.

Should my controller take the (res, req) params or already extracted params?

I wonder what way should I organize my routing in expressJS :
Params parsing in Controller
router.get('/users/:id', UserController.get);
class UserController {
get(res, req) {
var id = res.params.id;
UserModel.get(id, function(user) {
res.send(user);
}
}
}
Params parsing in Route
router.get('/users/:id', function(req, res) {
var id = req.params.id;
UserController.get(id, function(user) {
res.json(user);
}
});
class UserController {
get(id, fn) {
UserModel.get(id, fn);
}
}
I find the second version Params parsing in Route easier for
unit test
In case of change in the URL params or request body
but most of the example I found use the first version, why ?
If you consider a much larger, messier real world application, with route names that no longer match controller names etc., it might be beneficial to place the full routing table (all of the router.xxx calls) in one place, such as a routes.js. For a given url, this makes it much simpler for a new developer to figure out which code handles which url.
If you included all of the parameter parsing in your routes.js, it would become really messy and you'd likely lose some of the benefit of having collected all that into one file in the first place.
That said, there's no reason why you cant have the best of both worlds by separating the routing, the parameter parsing/response formatting, and the controller logic each into their own modules.

How to use params to filter database objects with Sailsjs

I'm still a bit new to Node in general, so I'm sorry if this is noob question. My setup is Sailsjs + MongoDB.
I have a RESTful API controller set up to handle the "lab" collection in my DB.
Here is what I use in my controller to pull up all the objects in this collection when /lab/ is used:
index: function (req, res, next) {
Lab.find(function foundLabs(err, labs) {
if (err) return next(err);
res.view({
labs: labs
});
});
},
In this collection there are fields for "site" and "lab" and I'd like to be able to filter what shows up with params like:
/lab/:site
/lab/:site/:lab
So if "/lab/aq" was pulled up it would get all objects in the AQ site and if "/lab/aq/123" was pulled up it would get the objects for the 123 lab in the AQ site.
I know this would likely be done with the Lab.find function, but I haven't been able to find any documentation which gives me an answer I'm looking for.
Any help would be appreciated.
In your config/routes.js file you need to add a route with optional parameters:
'/findLabs/:site?/:lab?': 'LabController.findLabs'
// use a different route than 'lab' to avoid interfering with the blueprint routes
Then, in your LabController.js, if the requested url had site and/or lab, you will find them in req.params:
// when a request is sent to '/findLabs/aq/123':
findLabs: function(req, res, next) {
sails.log(req.params) // {site: 'aq', lab: '123'}
// you can use them to filter:
var query = Lab.find();
if (req.params.site) query.where({site: req.params.site});
if (req.params.lab) query.where({lab: req.params.lab});
query.exec(function(err, labs) {
if (err) return res.serverError();
res.json(labs);
});
}

Express.js routing with optional param?

I have two situations to get data from DB
To show normal data
http://exampleapp.com/task/{{taskId}}
To edit data via posting
http://exampleapp.com/task/{{taskId}}/?state={{app.state}}
Both url have the same http://exampleapp.com/task/{{taskId}} just a little bit different with last phrase ?state={{app.state}}
I use Express routing as followed:
app.get('/task/:taskId/(?state=:status(pending|cancel|confirmed|deleted))?', routes.task.show);
But I dont know why it does not work ?
For example error: Cannot GET /task/51d2c53f329b8e0000000001 when going to h**p://exampleapp.com/task/51d2c53f329b8e0000000001
Query strings cannot be defined in routes. You access query string parameters from req.query.
app.get('/task/:taskId', function(req, res) {
if (req.query.state == 'pending') { ... }
});
However, if you're modifying a task, this is not the appropriate way to do it. GET requests SHOULD be idempotent: the request SHOULD NOT modify state. That's what POST requests are for.
app.get('/task/:taskId', function(req, res) {
// show task info based on `req.params.taskId`
});
app.post('/task/:taskId', function(req, res) {
// set task `req.params.taskId` to state `req.body.state`
});
You could either have a <form> that posts to the task, or make an ajax request:
$.post('/task/1', { state: 'pending' }, function() { ... });
According to the Express API, you cannot mix RegExp routes with string routes.
You should do something like this (I'm assuming taskId is an integer):
app.get(/^\/task/([0-9]+)/(?state=:status(pending|cancel|confirmed|deleted))?, routes.task.show);
However, I don't see why you cannot only check if req.query.state is defined in your route. It's probably less error prone and easier:
app.get("/task/:taskId", function( req, res, next ) {
if (req.query.state) {
// Do things
}
next();
});
Your problem is that query strings are not considered in routing. You will either have to redesign your urls (ie, include the state into the url itself, instead of the query string) or check the query string in your route handler function.

Resources