7

In my express app, I have two routes as follows:

router.post('/:date', (req, res) => {

    // if date exists, redirect to PUT
    // else add to database

})

router.put('/:date', (req, res) => {

    // update date

})

If date already exists on a POST call, I want to redirect to PUT. What is the best way to do this using res.redirect?

In the docs, all redirects are to different URL patterns. I would like to keep the URL same and change the rest verb from POST to PUT.

I had a look at this SO question, and added this line in POST:

res.redirect(303, '/:date');

but it did not redirect me to PUT.

software_writer
  • 3,941
  • 9
  • 38
  • 64
  • 1
    I would extract post and put handlers main logic as named functions and then just call respective functions from post or put handlers according to conditions. No need to redirect – Oleksandr Poshtaruk Feb 25 '18 at 20:35
  • Exactly. That's what I ended up doing. I was just curious if there is a way to do this though. Thanks – software_writer Feb 25 '18 at 22:25

1 Answers1

5

What you're trying to do here will not work for several reasons, but lickily you don't need to do any of that - see below.

First problem

The 303 "See Other" redirect that you're using here, by the spec should always be followed by a GET (or HEAD) request, not PUT or anything else. See RFC 7231, Section 6.4.4:

The relevant part:

The 303 (See Other) status code indicates that the server is redirecting the user agent to a different resource, as indicated by a URI in the Location header field, which is intended to provide an indirect response to the original request. A user agent can perform a retrieval request targeting that URI (a GET or HEAD request if using HTTP), which might also be redirected, and present the eventual result as an answer to the original request. Note that the new URI in the Location header field is not considered equivalent to the effective request URI. [emphasis added]

Second problem

The other popular types of redirects - 301 "Moved Permanently" and 302 "Found" in practice usually work contrary to the spec as if they were 303 "See Other" and so a GET request is made.

See List of HTTP status codes on Wikipedia:

This is an example of industry practice contradicting the standard. The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302 with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 to distinguish between the two behaviours. However, some Web applications and frameworks use the 302 status code as if it were the 303. [emphasis added]

Third problem

There is a 307 Temporary Redirect (since HTTP/1.1) but it explicitly disallows changing of the HTTP method, so you can only redirect a POST to POST, a PUT to PUT etc. which can sometimes be useful but not in this case - see Wikipedia:

In this case, the request should be repeated with another URI; however, future requests should still use the original URI. In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. For example, a POST request should be repeated using another POST request.

This 307 redirect is still not what you want and even if it was, it is not universally supported as far as I know so it needs to be used with caution.

See also this answer for more info:

Your options

You can abstract away your controllers - which you will usually do anyway, for anything complex:

// controllers - usually 'required' from a different file 
const update = (req, res) = {
  // update date
};
const add = (req, res) => {
  if (date exists) {
    return update(req, res);
  }
  // add to database
};

router.post('/:date', add);
router.put('/:date', update);

Or you can abstract parts of your controllers as functions.

Universal controllers

Also, note that you can write universal controllers called for every HTTP method that might work here:

router.use('/:date', (req, res) => {
});

REST

Note that what you're doing here is not a usual RESTful way of naming your paths and it may make sense to use only PUT in your case for both new and updated dates.

Contrary to what many people think PUT doesn't mean UPDATE. It means to put a resource (new or not) to a certain URL (overwriting the old one if it already exists). It's pretty much like writing this in the shell:

echo abc > /the/path/to/file.txt

which will "update" the file if it exists, but it will also create a new file if it doesn't.

So for example, if you have /users/:id path, then you use:

  • GET /users to get a list of users
  • GET /users/:id to get a specific user with that ID
  • POST /user (not /users/:id) to create a new user without providing ID
  • PUT /users/:id to overwrite an existing user or to create a new user providing an ID
  • PATCH /users/:id to update the provided fields of user with that ID

Here, as I understand your :date is like an ID, ie. you want to overwrite the record if it already exists and create if if it doesn't exist. In both cases you are providing the :date path component so you might use PUT for all cases just as well.

In other words, you cannot redirect from one HTTP method to another HTTP method (except for GET) but you don't need to do it in this case.

Community
  • 1
  • 1
rsp
  • 107,747
  • 29
  • 201
  • 177
  • 1
    yes yes yes on the last 'REST' point,. OP should straight up use `PUT` for this. In fact, I would go as far as saying that `PUT` should always be the default for 'create' if a natural or client-determined key is possible, and only use the non-idempotent POST if PUT is not feasible. – Evert Feb 25 '18 at 23:56
  • @Evert I agree 100%. Literally half an hour ago I read [an article](https://hackernoon.com/o-api-an-alternative-to-rest-apis-e9a2ed53b93c) on Medium to which few people have commented that PUT is always UPDATE (even providing mnemonics, like PUT = Please Update This etc.) People are always spreading this mantra and then everyone gets the wrong impression that PUT cannot create resources and it leads to confusion like this question when there is no need to redirect anything or share controller code because it's a perfect use case for a single controller with a single HTTP method. – rsp Feb 26 '18 at 00:04
  • Thanks a lot for the detailed answer and for the additional info! The first option you provided works best for my specific case. – software_writer Feb 26 '18 at 18:53