3 bad REST endpoint designs
While REST defines useful constraints, there are still plenty of way to shoot yourself in the foot. Let’s consider an use-case and attempt to write a REST endpoint for it. We’ll strawman some bad designs in order to get to a final good one.
Sample use-case
Let’s say we are tasked to implement an endpoint that allows users to delete their accounts. However, due to some manual business processes, user accounts cannot be deleted immediately. The actual business process is:
- User requests account to be deactivated (👈🏻 we are only designing this one)
- Support staff perform deactivation procedure
- Sometime later, user account is deleted
We are only designing an endpoint for step 1.
Attempt #1, delete the user
One of the design principles of REST is to use the HTTP semantics, so let’s just use the DELETE
method!
DELETE /users/:userID
content-length: 0
Unfortunately, this design is for an endpoint to delete the user, whereas we want to request a deactivation. This would be a suitable endpoint design when we are actually ready to delete the user after the deactivation process has been completed.
Further, if this endpoint did exist and but the user account has not been deactivated yet, it should error with:
400 BAD REQUEST
content-type: text/plainUnable to delete user. User account must be deactivated first.
Attempt #2, put the user into a deactivating state
OK fine. Let’s add a state
field on the user and let it be an enum of active
, deactivating
or deactivated
. Then we can PATCH
the state
to be deactivating
.
PATCH /users/:userID
content-type: application/json{
"state": "deactivating"
}
This is better, but this design ends up making the user resource a god object, in which we keep adding new features to a single resource. It would be more maintainable if features could be added more independently.
God objects also tend to bloat the GET
of a resource or worst leak sensitive information if public & private data are stored in the same place.
The ability to PATCH
the state
also implies a user could change their state
back to active
. A guard would need to be implemented to prevent a user from updating their state after the deactivation process has begun, as that would otherwise violate the business process.
Lastly, this API style is passive. The mental model here is by updating the state, some other process will detect/query for update.
Attempt #3, send deactivate command
Fine! Let’s make write an active command to simply deactivate the user.
POST /users/:userID/deactivate
content-length: 0
This is again better, but semantically this means “deactivate now”. Only a small tweak to get to the final solution.
Final solution, send request deactivation command
POST /users/:userID/request-deactivation
content-length: 0
This may seem like a pedantic distinction, but this is why naming things is hard. Human languages are imprecise and we need to put in extra effort naming things. A good name is the first step towards understanding and ensuring the intended functionality is maintained.
Holup, but isn’t POST
only for creating new resources in REST? REST tells us to write semantic HTTP, so let’s see what the HTTP spec says about POST
.
POST: Perform resource-specific processing on the request payload.
POST
can be used to submit commands to resources.
Do you want write semantic HTTP? You’re in luck, Battlefy is hiring.