Computers don’t care about API design. Convenient serialization formats, sane parameter names, and a RESTful interface mean nothing to a robot. These aspects of an API are not meant for a machine, they’re meant for you – the developer. APIs should be for human beings first and computers second.
Write, rewrite, ask, re-rewrite
Don’t settle on a design until as many eyes and minds have seen it as is reasonably possible. If you were given the task “Design a way for customers to search for phone numbers which they can purchase,” what would you end up with? There’s no single design that satisfies this requirement and, as a result, you can build any number of reasonable APIs.
For this reason, the AvailablePhoneNumbers resource was challenging to design. A major rule we follow in API design is make the common case easy. When searching US numbers most users end up wanting one of two things: A number from a particular area code, or a number containing some string of digits. These are the first two filters we document and the ones we expect most people to care about.
Why even include an area code parameter? You can accomplish the same with the ‘Contains’ parameter and wildcards , i.e., +1555*******. The first iteration of the design only included a Contains parameter. It was simple and powerful. It was also frustrating when all you needed was an area code lookup as you had to remember the exact filter syntax. If you have to think this much to complete a dead simple task, then we’ve failed as API designers.
Simplify the surface area of your API
Let’s take a look at the IncomingPhoneNumbers resource. A POST to this resource will let you buy a number, and a GET to this same resource will let you see the numbers you’ve bought. When designing this functionality, the first instinct of many developers would be to create a separate resource for purchasing phone numbers. POST /ProvisionPhoneNumber to buy, and then GET /IncomingPhoneNumbers to return a list of numbers you’ve bought. However, we now need an entirely new resource to provision phone numbers that supports only one method. When you take this design and multiply it by the amount of functionality you wish to provide, you end up with an explosion of complexity.
GET /Calls POST /PlaceCall GET /SMS/Messages POST /SendSMS GET /IncomingPhoneNumbers POST /ProvisionPhoneNumber
GET|POST /Calls GET|POST /SMS/Messages GET|POST /IncomingPhoneNumbers
By choosing a consistent convention we’re able to simplify the surface area of the API. When we add things like Subaccounts, it’s a natural extension of the API. POST /Accounts to create a subaccount, GET /Accounts to see your accounts. In addition, every unique parameter name adds to the weight of your API. Be consistent and try to reduce the number of resources a client of your API has to learn. Put yourself in the shoes of your users.
Easy to add, Hard to take away
There is a fine line to walk when adding new features. If you have a particularly contentious feature design, don’t be afraid to leave it out. After you launch with it in your public API, it is extremely difficult to remove features without a) bumping the API version or b) breaking customer’s applications. This means that every addition should be reviewed with the knowledge that removing or changing the API later could be more difficult for customers than not having it in the first place. At Twilio we’ll often add extra parameters, resources, or properties as we see customers adopting and using an API instead of attempting predict all usage patterns from the beginning.
Even after all of that, mistakes will happen.
Don't be afraid to make backwards incompatible changes
In the long run, it’s better for your user base and it’s better for you. Sometimes your assumptions are wrong, sometimes a parameter name isn’t the clearest, sometimes your response format is less than ideal.
However, once you’ve released a new API version, it’s very important to maintain access to the old API version for some period of time. It’s very difficult to get customers who have built against a certain API version to upgrade. You can provide incentives: new features, better performance, etc. Even then, many customers who’ve already built a working integration are reticent to incur the cost of upgrading.
One way to reduce the cost is to make version changes as transparent as possible. By writing smart client libraries, you can abstract away certain aspects of API versioning. For example, a client library could turn requests like this:
responseText = client.request("/2010-04-01/Accounts") d = parseXML(responseText) a = d.xpath("//Accounts")
accounts = client.getAccounts()
Now, let’s say you add support for a JSON representation in addition to XML. You can beg all of your customers to switch, but almost no one will if they’ve implemented the first case. In the second case, everyone can get the upgrade for free. In fact, if your client library was smart enough to send the Accept: application/json; text/xml; header The server could decide at request time which representation was most efficient for this particular request and dynamically send the better Content-Type. The user of the library wouldn’t ever need to know which response type he’s receiving and he wouldn’t ever need to upgrade his library to handle one or the other.
Human-centric API Design
API design is as much an art as a science. There isn’t a single ‘right way’ of doing it. Different APIs feel and behave in different ways. We’ve grown the Twilio API from only a couple of features to over a dozen, and we continue to add new features and functionality all the time. At the end of the day, we build our APIs to serve you: the human being who needs to get stuff done.