Hacker News new | ask | show | jobs
by fzilla 3573 days ago
Other commenters are correct that POST /accounts/4402278/close is not right (and also fairly hilariously contradicted in the next section).

Account status (open, closed, suspended, whatever else) is a property of the account, in the same way that the account owner's name is a property of the account. If you went to all the trouble to represent each account as its own resource, which I assume responds correctly otherwise to GET, POST and PUT requests, why is this one property special enough to get its own endpoint? Why would you not just PUT or PATCH the account to change the "status" property to "closed"?

In my experience, programmers typically break best practices in this way when there is special logic that needs to happen when the property changes. In other words, PUT is fine as long as it's only overlaying new data and not triggering other processes, but closing an account kicks off a whole host of internal processes at the business, so it seemed reasonable to someone to make it a separate endpoint.

This either represents a friction between good API design and what programmers find reasonable ("I have to make a bunch of special things happen, so I'll bundle them into their own function and expose them"), or the API framework isn't flexible enough to supply hooks to insert logic at field-level changes, or both.

3 comments

This depends on your data model.

If "status" is just a property on the accounts resource and doesn't have further meaning, I would tend to agree with you.

If "close" is an action or activity that acts upon an accounts resource, then his approach makes sense.

Since the context is an account that we "need to close," I would assume the author is talking about something more complex than a database field. It's probably a workflow that initiates other actions and workflows, maybe even requiring additional review. I'd want to see/understand the requirements more fully before I started down either path.

REST is just a representation of the underlying data. What does it matter if "status" is a database field or not?

More specifically, it shouldn't have to matter to consumers. Because of the way REST and HTTP work, clients intuitively understand retrieving and modifying resources (via GET, POST, PUT, PATCH and DELETE). But they don't understand interacting with special-purpose endpoints (POST always implies "make a new thing" so it's weird in this context).

Your consumers should not have to learn weird idiosyncrasies in your API because you let your data model bleed into the interface.

No, REST and HTTP are implementation details of the technology.

An API is a representation of business workflows and processes. The more accurately you describe or map those to the real world processes, the better your and your consumers' understanding will be.

What if 'status' isn't a column in the account database? What if closed_accounts is a join table or something else? Then wouldn't adding a /close route be hiding idiosyncrasies in your data model, not the other way around?

It seems like we spend a lot of time designing object relations that map a domain, but maybe not so much mapping domain-specific actions.

Or would you consider all of the above bad practice?

This doesn't really matter. What you'll end up exposing via your API is your use case! Does your system allow for an account to be closed? Awesome! So let the client know how to do it.

If you're speaking HTML it can be as simple as:

  <form action="/close-account?acc_number=12345" method="POST">
    <button>Close Account</button>
  </form>
Or even:

  <form action="/close-account" method="POST">
    <input type="hidden" name="acc_number" value="12345">
    <button>Close Account</button>
  </form>
If you're speaking Siren (https://github.com/kevinswiber/siren):

  {
    "actions": [
      {
        "title": "Close Account",
        "method": "POST",
        "href": "/close-account",
        "type": "application/x-www-form-urlencoded",
        "fields": [
          { "name": "acc_number", "type": "hidden", "value": "12345" }
        ]
      }
    ]
  }
> What if 'status' isn't a column in the account database? What if closed_accounts is a join table or something else? Then wouldn't adding a /close route be hiding idiosyncrasies in your data model, not the other way around?

So what? REST is an API [0]. It's the public interface you expose. What you do behind the scene in your data model is, or should be, irrelevant to the API. Sometimes you'll map your data model practically one-to-one to the REST API, but there are times when I do significant logic in controllers before mapping the result of that logic to a resource. As long as the API is resource oriented and follows good REST practices, it's all good.

0. OK, REST is one way to expose an API, since the wording I used is not amenable to some people.

> REST is an API.

No, REST is an architectural style.

> No, REST is an architectural style.

For APIs. But yes, we can argue semantics now. My point is, REST is a way to expose an API.

> If "close" is an action or activity that acts upon an accounts resource, then his approach makes sense.

Right. Particularly, if a closure request is a thing that has its own status, identity, and associated data elements, which one might wish to examine and interact with (and, while its possible that such interaction might not be possible for external users with privileges only on their own accounts, it might well be possible for internal administrative users), then it makes sense as a resource, rather than a data element buried in representations of another resource.

Without full behavioral requirements and domain model, you really don't know what makes the most sense.

Certainly, though I am skeptical of that theory being true in the author's actual case. Because if it was, I would think the plurality in the URI would be consistent, and instead of

POST /accounts/4402278/close

You would have something like

POST /accounts/4402278/closures

Indeed—I would tend to argue that in most cases updating the value of an attribute on a resource for a complex operation is itself an antipattern.
The action is "close". The status may or may not be "closed", but that's not what he's doing. Read a little farther. In the brief example JSON for the accounts, there is no "status" property. In fact, there are four actions available on the account:

    {"rel": "deposit", href: "/account/4502278/deposit"},
    {"rel": "withdraw", href: "/account/4502278/withdraw"},
    {"rel": "transfer", href: "/account/4502278/transfer"},
    {"rel": "close", href: "/account/4502278/close"}
Yeah I'm saying this is wrong :\

This isn't how REST is supposed to work

Ah, and therein lies the classic problem. Nobody is doing REST "right," but I'm yet to see anyone point to a de-facto example.

Fielding's dissertation isn't a spec, which pretty much means everyone can come up with their own little slice of how it should be done and then say they're doing it right.

The true rule of REST: whoever is blogging/commenting about it at the time is doing REST right, all others are confused and incorrect.

There is a lot ceremony with "REST". Considering REST is architectural pattern and not even an HTTP-specific one I find it very suspect whenever someone says REST API's must use particular HTTP methods or structure URL's in a specific way.

Conventionally that is true and it certainly reduces friction in your API by adhering to common conventions.

Reminds me of Agile...
Can you give an example here of how you think it should work, please. This is one area of REST with which I have a lot of trouble.
One way to approach it would be to have consider deposits, withdrawals, and transfers as subresources of a specific account. So you could POST to "/account/4502278/deposits" to create a new deposit, which would then live at a URL such as "/account/4502278/deposits/87162". And instead of separate subresources for all of these different transaction, it could just be one "transaction" subresource.

In the case of closing an account, it depends on what actually happens when you do so, but I would normally have a "status" attribute and use PATCH to update that.

A less related question: how do you decide between adding something as a property vs. creating a new subresource?
Good question, I guess it's pretty much the same as deciding if something is a new field or a new table in a database?
Your PATCH example makes sense, so I should do something like (depending on your opinions of how to do PATCH requests):

    PATCH /users/123

    [{ "op": "replace", "path": "/accounts/12345/status", "value": "closed" }]
I personally think that's less readable than the original, but I agree that it seems to more closely fit the REST standard.

With the deposit, I don't really understand what's changed here from the original. To me, it just looks like deposit has been pluralised, and everything else is the same:

    POST /account/12345/deposits
 
    amount=10
I would simply PATCH { "status" : "closed" }, as that's how PATCH works in rails.

Regarding the deposits, the difference is that 'deposits' is a collection of deposit resources vs 'deposit' as a verb. I think in this case it makes a lot of sense to do it that way, as you can then GET /account/12345/deposits and see all the deposits ever made.

For the deposit/withdrawal/transfer set, depending on the specifics of how they are actually moving the amounts around, would likely be best served as their own resource. Since a deposit/withdrawal is just a transfer anyways, just having "/transfer" would be likely be good. The connotation here is that the client is creating a transfer on the server. The body for this request could take the relevant account numbers (you need two accounts), and the sign on the amount would denote if the amount is being deposited or withdrawn.
So I would make a request like:

    POST /transfer

    from_account_id=12345&to_account_id=67890&amount=10
Perhaps I've misunderstood your comment, as I don't really understand why this is better than the original:

    POST /accounts/12345/transfer

    to_account_id=67890&amount=10
or even:

    POST /accounts/12345/withdraw

    amount=10
Having the /account/:accountid/transfer is not a terrible idea, but I would avoid it because the transfer resource is able to stand on it's own with making it tied to a single account resource.

The key point though is that 'transfer' is the resource to perform all three of the original deposit/withdrawal/transfer actions.

What HTTP methods should be callable on those URL endpoints? Presumably POST should trigger the action it is not idempotent, but what content should we be POSTing?
> Other commenters are correct that POST /accounts/4402278/close is not right (and also fairly hilariously contradicted in the next section).

Of course its right. Read the POST spec. POST is for processing data. Its up to the server to what is processed how. If the POST is for closing a bank account, it is valid. I think, my bank would need verify a lot of things before I can close my account with a single click. So the operation cannot be idempotent and thus requires a POST.

I mean, if you are fine with setting just a flag in your bank, you can be fine with a PUT. But I will not become a customer of your bank.

Just because a flag can be set with PUT doesn't mean the server has to accept it in all circumstances. Maybe there are preconditions elsewhere that must be met first, or maybe only someone with sufficient access credentials can set the flag. This plays pretty well with PUT.

Again, this is the interface to a complex data model, and I would be wary of using a bank that dumped all of its security and process controls into one endpoint's controller.

Also - closing an account is an inherently idempotent operation, no? It can only be closed once. If I request that a closed account be closed again, it stays closed.

A PUT means that the server accepts the payload as is. Yes, it can be restricted as is. But the request must be free of side effects. The server should not even validate the payload. And that also means that your request should not trigger stuff that cannot be triggered again. If your model _only_ relies on that flag, than the PUT is fine. But when I close a bank account, usually I trigger stuff on the server side (including validation). So a POST should be required.