Node/express, optional parameters switch route between get and post - node.js

I have 2 routes set up for my express server that look very close to each other. They are basically the same url, except one is post and one is get, and the get has an extra route param (which is optional). Right now these seem to work ok, however if I do not add the optional param to the get call, it thinks I'm trying to hit the post. I would like to be able to hit the get call without the passing the second optional param as well. Let me show you what I have so far:
router.param('itemID', (req, res, next, itemID) => {
verbose("itemID=", itemID);
next();
});
router.param('navigationType', (req, res, next, navigationType) => {
if (!navigationType) {
next();
}
verbose("navigationType=", navigationType);
next();
});
router.route('/:itemID/navigations')
.post(controllers.addActivity)
.all(routes.send405.bind(null, ['POST']));
router.route('/:itemID/navigations/:navigationType')
.get(controllers.listActivities)
.all(routes.send405.bind(null, ['GET']));
The routed.send405 method looks like this :
function send405(methods, req, res) {
res.set('Allow', methods.join(','));
res.status(405).json({
message: `Method '${req.method}' Not Allowed.`
});
}
So right now the issue is if I do a get on /blah123/navigations and don't add the /:navigationType variable, it thinks I am trying to hit the post method. I am very new to working with this and would appreciate any help or insight. Thanks!

When you declare a route, say GET /admins/:id, it will match any requests to GET /admins/1 or GET /admins/john. But when you do just GET /admins, it wouldn't be able to find because you haven't declared GET route matching that pattern.
To work with this, you have to specify navigationType is an optional parameter and also place the GET request first followed by the POST, like this.
router.route('/:itemID/navigations/:navigationType?')
.get(controllers.listActivities)
.all(routes.send405.bind(null, ['GET']));
router.route('/:itemID/navigations')
.post(controllers.addActivity)
.all(routes.send405.bind(null, ['POST']));

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.

Handle POST request from multiple routes all at once

I am submitting a simple contact form in my website's footer (in footer.pug):
form(method="POST" action="contact_form")
input(type='email' name='ct_email' data-name='ct_email' required)
textarea(type='text' name='ct_message' data-name='ct_message' required)
button(type='submit') Send
Since the form is in a template, and the footer template is used throughout the site, the form can be submitted from various routes:
/contact_form
/route1/contact_form
/route1/de/contact_form
and so on...
So now it seems I have to create a handler for all the possible routes:
router.post('/contact_form', function(req, res, next) {
// ...
}
router.post('/route1/contact_form', function(req, res, next) {
// ...
}
How can I easily handle POST requests from all the routes they may be coming from without writing a handler for each?
You can use absolute path reference in your form and it will always submit to the same route even though the form is in different pages.
Try this
form(method="POST" action="/contact_form")
Notice the action changed from contact_form to /contact_form. When you add /, you start referencing the path as an absolute path to the domain. So now, from all pages, the form will be submitted to http://your-domain/contact-form.
Not entirely sure if this is what you mean, but the first argument to ExpressJS's router (I assume that's what router is doing here) can be an array. So instead of:
router.post('/contact_form', function(req, res, next) {
// ...
}
router.post('/route1/contact_form', function(req, res, next) {
// ...
}
You can just do:
router.post(['/contact_form','route1/contact_form'],function(req,res,next){
//some fancy logic to handle both routes.
})
Of course, this requires that you keep a list of these possible routes. On the other hand, you can follow Dinesh Pandiyan's advice, and just use an absolute path. So instead of page1.html, page2.html, page3.html, etc. all having their own own router (or own entry in your router array), you'd essentially be saying "Go to the domain route, then go to this address".
Each request should be handled in separated functions because each request has its own logic. However if you want
function request(req, res, next) {
// Your logic
}
router.post('/contact_form', request) {
// ...
}
router.post('/route1/contact_form', request) {
// ...
}
Right now, I don't have a way to test this code, but I think that will help you.
Here is yet another potential solution - use an independent function as a route handler.
router.post('/a', handlePost);
router.post('/b', handlePost);
router.post('/c', handlePost);
function handlePost(req, res, next){
// use req.path here to figure out what url was called
}

How to work with implied function parameters in NodeJS

In my website's routes file, I have a function like this:
router.post('/', ctrl1.validate, ctrl2.doSomething)
With the validate function looking like this:
function(req,res,next){
var errors = validator.checkForm('myForm')
if(errors){
res.redirect("/")
}else{
next()
}
}
If I want to pass parameters into the validator function (like the name of forms I want to validate) besides the implied req,res,next, how is that done? I have tried ctrl1.validate(formName) and ctrl1.validate(formName, req,res,next) with function(formName, req,res,next) in the controller, and neither work.
The ideal solution would be to identify what form you're working on from the data passed with the request in the first place. You don't show what that is, so we don't know exactly what to recommend or if that is feasible in this case.
If you can't do that and want to have a generic function that you can use as a request handler in multiple places and you want to pass a parameter to it that is different in each of the different places you use it, then you need to create a function that returns a function.
router.post('/', ctrl1.validate("someFormName"), ctrl2.doSomething)
// definition of ctrl1.validate
validate: function(formName) {
// return a request handler that will knkow which formName to use
return function(req,res,next){
var errors = validator.checkForm(formName)
if(errors){
res.redirect("/")
} else {
next()
}
}
}
When you first call this method, it returns another function that is your actual request handler. That inside function then has access to both req, res, next from the request and has access to the formName that was originally passed in.

Dealing with slash characters in request parameter using Express route

I'm currently working on a URL shortener app using Express.
I want the user to be able to enter a URL like this:
https://www.exampleurlshortener.com/new/https://www.google.com
The problem is whenever I try to specify the parameter using Express it will only extract the 'https:' section and everything after that is lost because the 2 backslashes are registering as a new route:
app.get('/new/:url', (req, res) => {
console.log(req.params.url) // outputs 'https:'
I thought about specifying each section as a new parameter but if inner is blank this ends up throwing a 404. I would need to check if inner is blank using this method otherwise the user would be able to type https:/something/www.google.com
app.get('/new/:prot/:inner/:address', (req, res) => {
// throws 404 on valid addresses
Is there a simple way to solve this that I'm missing? Is the full URL available to be checked somewhere in the request? Or can parameters ignore backslashes?
You can use an expression to for your URL placeholders:
app.get('/new/:url(*)', (req, res) => {
console.log(req.params.url) // will output 'https://www.google.com'

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