5

Most of us know SOLID, and over the years have understood how useful it can get when we need to change.

Based on S & I parts of SOLID and from experience I used to design my HTTP RESTful services as fine-grained as I could. For example, in a simple contacts management system, I would create and expose these endpoints:

  • createContact?firstName=x&lastName=y
  • addPhoneToContact?contactId=x&phone=y
  • addEmailToContact?contactId=x&email=y
  • associateContactWithGroup?contactId=x&groupId=y
  • removePhoneFromContact?contactId=x&phoneId=y
  • updateContactName?contactId=x&firstName=y&lastName=z

However, recently we were arguing over the granularity of our RESETful services, and a colleague proposed that we see Google's Contact as a sample and model.

To my surprise, I saw that they update contact in a very complex and coarse-grained manner, in one simple API.

We all have to admit that Google is the Internet's giant, and they don't do things without knowing what their doing.

If this was an arbitrary website, I wouldn't even consider them, and would probably argue that their developers are not familiar with the concept of breaking things down (divide and conquer/analysis/WBS/SOLID/...). But Google, is totally different.

Now I'm stuck at why they've done so, and which approach is more maintainable and better to create APIs. One big single API that updates/creates a complex model, or many small APIs that update/create small parts of that model.

This is a challenge for shopping carts, for contacts, for educational course management, for accounting articles and almost many more cases.

I know this might be subjective, but I'm searching to find objective reasons behind each approach, so that we can decide with more knowledge.

  • 2
    I am failing to see how SOLID factors in to this. Are you referring to the "S" (single responsibility principle)? – Becuzz Mar 9 '17 at 13:11
  • Single Responsibility & Interface Segregation. Maybe I should change the title to something like How to decide the granularity for RESTful APIs. Is that more clear? – Saeed Neamati Mar 9 '17 at 13:13
  • That sounds far better to me. – Becuzz Mar 9 '17 at 13:15
  • 3
    Something, something, REST endpoints generally should not contain verbs, those should be represented using the HTTP methods. – Andy Mar 9 '17 at 13:18
  • @DavidPacker, you're right. Though the question is more about breakdown structure, I can change them too in case they're misleading. – Saeed Neamati Mar 9 '17 at 13:22
6

This borders on a religious answer, but....

The examples provided in the question are not RESTful, to me. createContact is a verb, not a noun, and does not specify the request type. The REST paradigm would suggest:

POST /contact - data={firstname: 'blah', lastname: 'blah'}

The removePhoneFromContact?contactId=x&phoneId=y would become:

DELETE /contact/x/phone/y or DELETE /phone/y if the phoneId is globally unique

Similarly, the edit operations could be done by a PUT or a PATCH. The PATCH would be useful where you only want to change one field, and you aren't required to send the full resource. Things like etags and last-modified headers are used to ensure consistency.

Google also has many considerations that we Common Folk do not. The savings of a few percentage points in network usage may be far more important to them than the ease of understanding of an API.

3

I think maybe contacts is a bad example from the view point of SOLID. On first look 'Editing Contacts' seems like a single responsibility.

If we imagine the more general case though I think you have a good question. What objective factors can consider when deciding to split a responsiblity?

I think there are a couple of practical considerations.

  1. How big is your object/aggregate root? we have to hold this thing in memory and send it over the wire. At some some point its going to be too big to do those things effectively

  2. How frequently do we change it. Are those changes going to collide? if I'm adding addresses a thousand times per second from multiple clients, trying to change the entire object each time is likely to create collisions and be sub optimal compared to just AddAddress(address).

  3. How do we store the object. If the underlying storage is a no-sql db storing the entire object, the efficiencies of exposing AddAddress stop at this level. If you have a relational db which can add addresses to a table without looking at the rest of the data you want to be able to offer that value all the way up the chain.

I assume that google are using a no-sql db for contact storage, contact objects are small compared to bandwidth and memory and only the owner of that contact is likely to do updates, at a human clicking button rate.

2

As others have pointed out a RESTful API should expose resources (nouns) rather than actions. See Representational state transfer (Wikipedia).

But I understand that your question is about the granularity of the resources that the API exposes.

Let's take your contact and its email property for example. We'll start by creating a contact:

POST /contact 
{"name": "John Doe"}

(Please forgive a tangent since it's completely unrelated to your question, but I suggest not splitting names into first and last unless you absolutely have to and here's why).

This returns a new contact_id of 17.

So now the real question is, do we add a phone number to John like this?

PATCH /contact/17
{"phone": "555-5555"}

...or like this?

POST /contact/17/phone
555-5555

Reflect the data model?

I think one determinant may be your underlying data model.

If you have a contact database schema that looks like this:

=====================================
contact
=====================================
contact_id    name          phone
-------------------------------------
17            john doe      555-5555

Then I think the single /contact resource makes the most immediate sense.

But if your database schema allows for a one-to-many relationship with phone numbers like this:

========================
contact
========================
contact_id    name      
------------------------
17            john doe  



===================================
phone
===================================
phone_id    contact_id    number
-----------------------------------
243         17            555-5555

Then the more granular resource, /contact/17/phone, suddenly seems sensible. (After it's been created, this specific phone number would be at /contact/17/phone/243 on which you could act with HTTP verbs PUT or DELETE).

Of course, your API resources do not need to (and often should not) reflect your underlying data model. Both database schemas could be reflected just fine with either API structure. But it can be

Other considerations:

  • The API can hint at the way it should be used by the way its structured. /contacts and /contacts/{id}/phone may allow the same functionality, but they give the end API's consumer a different impression!
  • More granular APIs can conceivably allow you to create more efficient services by allowing you to write very specific bits of code at the expense of more code. /contacts/{id}/phone can be written very simply - but you are now saddled with the overhead of writing and maintaining (and documenting!) this resource for the lifetime of the API
  • You can add more granular resources while keeping backwards compatibility, but you cannot take them away! So if you're on the fence, consider starting off coarse and adding finer grain as needed!

Also somewhat tangential to your actual question is how to deal with this tricky relationship in REST:

  • associateContactWithGroup?contactId=x&groupId=y

You can either allow /contact/17 to take a {"group":"34"} property in an update. Or you can create a /contact/17/group resource. (This could serve a one-to-one or many-to-many relationship with groups, which would look very similar.)

The interesting thing is that you may also want to expose this from the groups point of view with resources like /group/34/contacts. There's no wrong answer, but consider having as few resources as you can get away with to start. You can always add more in a backward-compatible way later!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.