I thought it might be useful to walk through a simple use case that highlights some common design considerations when writing APIs. It would be really great to hear feedback and alternative views. Consider an API that allows the following:
- a user can view their own profile
- a user can update their own profile's avatar, email and password
- a user can view other people's public profile (a public profile of another user is a subset of the fields displayed in your own profile)
- an administrator can view any user's profile, which includes an additional field "accountStatus" (either "enabled" or "disabled").
- an administrator can change "accountStatus" in addition to modifying the other profile fields.
Naively a first attempt at modelling the above interactions may involve exposing the following resource:
/users/:userId/profile
You might choose to allow OPTIONS, HEAD, GET and PUT. On GET requests you would have to:
- determine who is calling you
- filter your representation based on who is calling you, including links, if any
- provide response
On a PUT, you would:
- determine who is calling you
- figure out what they're trying to change (which means fetching something from the database)
- apply rules based on a combination of (user, changes) to decide if what they're doing is allowed
- apply changes, if any
- provide response (which may just be a 204)
The implementation might get ugly, particularly step 3. It also produces design problems when you're trying to separate your API/HTTP code from your business layer because you need to load the resource just to isolate what's being changed. The GET operations are also applying conditional logic to decide what subset of the profile resource it should return. I suppose one way of addressing this is that you could write some middleware that maps (user, operation) to a function call specific to that use case. Does anyone know if this kind of routing pattern exists, btw?
Anyway, rather than having conditional logic in our code, we can use separate resources for each of the distinct use cases we have. So:
- GET/PUT /user/profile
- (read/update *your* profile)
- GET /users/:userId/profile
- (read another user's profile)
- GET/PUT /manage/users/:userId/profile
- (for admins, read/update a user's profile)
Now your implementation knows who should be calling and does not require as much branching logic when providing a response. This seems cleaner and less prone to change (or at least you're better positioned for change by addition). It also seems to encourage an API that is more focused on specific use cases and thus better at guiding clients.
I would even consider breaking this API down further. Consider the administrative task of enabling/disabling an account. This is likely a separate activity from other kinds of user profile changes and we may want to do some internal things when this happens (send e-mails, events, audit logs, etc). So maybe it is better to do something like:
GET /manage/users/:userId/profile
PUT /manage/users/:userId/profile (for updates to a user's profile, except the status)
PUT /manage/users/:userId/profile/status
A lot of this comes down to API granularity and what I guess you could call "use case orientation". It would be really interesting to hear feedback on what factors are important for breaking down a seemingly simple API like this one.