I recently had the opportunity to have a discussion about API design and RESTful services with Vasusen Patil. I’ve also been having quite a few conversations of similar nature with Justin Davis over the last few months. These types of conversations are some of my favorite because, for the most part, there are no right answers and many many interpretations.
I’ve been on both sides of these conversations as of late. At work, I am in the trenches with API consumption. With my PHP framework, Pickles, I have been in the middle of a rework that would move away from being a system for building websites and moving towards a system for building services.
At work I have been given the opporunity to see how upwards of 30+ different companies with similar products would implement the same API system. Even with OpenTravel standards, there is still quite a divide on how to do things. At this point, I’ve got my own thoughts and ideas on how to do things. I wanted to get them out of my head and into my blog for posterity’s sake. Let’s be honest, as engineers, our ideals tend to change over time and it’s always good for a laugh to look back at things ;)
I’ve been guilty of it in the past, but these days I will do whatever I can to
keep any verbs out of the endpoint names. Somewhat trivial, but I prefer my
endpoints to be all lowercase. When IDs are involved, I like to use integers
and I like them to occur after the noun. Example:
Even though PHP doesn’t do me any favors with
$_DELETE I do prefer
to break up actions by request method. My logic behind doing so is because I’m
not a fan of including an “action” parameter to determine what we’re doing.
It keeps the requests a bit cleaner but at the sacrifice of being harder to
implement in some languages compared to just using
JSON for life, or at least something better comes around. I’ve worked with enough XML to fill at least a dozen lifetimes and simply do not like working with it.
I like em and I use them. In addition to leveraging the status code in the
header, I also put it in the response body (as part of the
In addition to the status code, I always include a message, even if the call
was successful. The message is part of the
meta section of the response and
can be as terse as “OK” or as verbose as “The user profile was updated
successfully” or “The request was missing the %s parameter”. I am not a fan of
cryptic internal error codes and having to go dig back through documentation to
figure out what they mean, returning explanatory messages helps a ton.
Versioning as a whole could probably be it’s own blog post at this point. I’m
a fan of having a major version number (integer) as part of the request,
/v1/user for example. By itself this is great for systems that receive
updates infrequently. HolidayAPI is a good example as the API
hasn’t been changed in at least a year or so.
Also, there is a school of thought that the version in the endpoint is not as semantic as including it in the request header. I can definitely agree with that but at a certain point it’s just nice to look at the endpoint and know exactly what version you’re working with.
The major version number works for slow to update systems but can all fall apart when you have an API that is hacked upon daily. The major version number is still a good indicator of things, but you will probably need to include a revision number of sorts to do further version abstraction. This gets even more fun when you think in the scope of versioning the entire API or just the a specific call.
Once you’ve made the decision, how do you handle that in your code? This is why I enjoy these kinds of conversations, they can be as mind boggling as you want them to be :)
Yes. Even if the rates are very high, you still need something in place to keep abuse to a minimum. At this time I’m actually unsure if rate limiting should be built into the service’s code or should be handled by the server with something like this.
Going back to versioning, I prefer that each version of my API does not rely on any code from the previous version. If that means just copying over the entire system as your first step so be it. It’s not pretty but it eliminates the situation where a change in one version could impact the other version which is not ideal at all.
To take this a step further, I like each of my endpoints to function without anything from another endpoint. Same logic as above, I don’t want to run into issues where the dependencies break other calls. Still not pretty but it gives me piece of mind. I still have some reuse by way of using the same ORM and utility functions, so it’s not a complete vacuum chamber.
This lack of reuse actually stemmed from how I started writing my unit tests recently. With my tests, I didn’t want to have to run the entire harness just to test a single class. Because of this, I started to write each test class to provide 100% coverage for the entire file, instead of relying on tests in another class to fill in the gaps. It results in more tests but it means if you drop a class and tests from your system, the code coverage will not dip.
This is a fun one because I have a few assumptions and then some blank spots. When adding a new record or editing the record, it’s a no brainer, you can return the ID and/or the entire record (which could then be used as an echo). What about when you delete a record? I’m actually still not sure on this one. You could return the ID and/or the entire record just for consistency with your other calls. You could also omit the response body entirely because you deleted the record and there is nothing to give back. Another route would be to include “deleted” in the response or perhaps in the “meta” section and continue to leave the body blank. Only The Shadow knows.
I’ve already mentioned it, but I like to break my response into a “meta” section and an optional “response” section. Meta contains everything that’s not related to the data being returned. Including but not limited to the status code, message, API version / revision, any echo token that was provided, et cetera.
At the end of the day, the only truth for me is that there aren’t really any wrong answers as long as your API performs well. If it doesn’t perform well at the very least degrade gracefully. I run into a lot of APIs that simply can’t handle a flood of calls and it cripples their entire API. A lot of times the “flood” of calls I’m making is one every couple of seconds, far from an onslaught ;)
Would love to continue this conversation down in the comments, feel free to join in!