Article
RESTful API Design Tips for Digital Products
February 17, 2021
You’ve done this before. You’re designing the contract for sending data between your server and your mobile app, and you’ve followed all the widely adopted REST best practices. Your API has all the following:
- Beautiful, comprehensive documentation.
- Consistent models, request bodies, and response bodies. Not just for data, but for errors, too.
- Conventional HTTP status codes that are impossible to misinterpret.
- Version info in the request URL, not the headers.
- And a long list of other features based on best practices.
Even so, you might be missing a few things.
At Livefront, we’ve been designing, building, and shipping mobile apps and their accompanying APIs for over a decade. We’ve cultivated a list of our own conventions that cater to the unique constraints of apps and other digital products. What follows are a few of our favorites.
Build in Support for Sunsetting
Unlike a website, when you release a mobile app, that app will exist “in the wild” indefinitely. iOS and Android support auto-update, and it’s enabled for most users, but it’s not possible to guarantee or force updates unless you design and implement a kill switch.
Not sure you’ll need it? You will. And it should be part of your API from the beginning.
Here are some common scenarios:
- You ship a hotfix for a critical bug in production. To minimize impact, you want to encourage or require users to update.
- One of your third-party dependencies breaks or is discontinued, and you need to fix or replace it.
- You release a new version of your product and want to discontinue the old one.
- You discontinue support for a previous version of your API.
- Your product’s name or branding changes and coincides with a marketing push. e.g. HBO Now to HBO Max.
- Your product transitions from one thing to another. e.g. Sonos S1 to Sonos S2.
- Your product reaches its end of life. Even removing it from the App Store won’t prevent people that previously downloaded it from continuing to use it.
You won’t need it often, but the only way to ensure that you have the option to sunset when you need it is to include it with your first public release.
Document Rules for Backwards-Compatibility
This simple technique allows teams to agree on which API changes will be supported without breaking existing apps. A simplified version of our agreement is:
It’s okay to add new resources and attributes, but it’s not okay to rename or remove things. If an API change ever breaks the rules, we introduce a new API version.
In practice, it’s almost always possible for us to abide by the rules, and we rarely introduce new API versions. Documenting and enforcing the agreement lets us iterate on the product without breaking existing apps that rely on the API. Server developers can’t introduce breaking changes, and client developers can anticipate and support changes they know are allowed.
Use Custom User-Agent Strings
The default User-Agent strings on Android and iOS aren’t as helpful as they could be. We use custom User-Agent strings, which enable the server to:
- Improve logging and diagnostics related to client requests.
- Customize responses and other behavior based on which client made a request.
- Respond appropriately when a product version is sunsetted as described above.
We use variations of the following template depending on a given product’s requirements:
[App Name]/[Version]/[Build] ([Platform]; [OS Version]; [Model])
For example:
Taco Tracker/1.5.0/202012201 (iOS; 14.3; iPhone13,1)
Provide Type Hints for JSON Models
JSON has very limited support for types. Consider the following JSON response:
{
"foods": [
{
"name": "Matt's Apple",
"variety": "Pink Lady"
},
{
"name": "Nel's Taco",
"spiciness": "medium",
"condiments": [
"avocado",
"pico de gallo"
]
}
]
}
It’s technically valid, but this pattern makes deserialization tricky in strongly-typed languages like Kotlin and Swift. It groups multiple types (apples and tacos) together in the same array. There’s no way clients to know whether a given food is a taco without searching for the spiciness or condiments attributes. And what if more than one type of food can be spicy and have condiments?
Clients should never be required to interrogate payload attributes to determine a type.
One simple way to remove this ambiguity is to include a wrapper object that defines the type. In the following example, type
is an enum that matches the key of a nested object of that type. Clients don’t have to guess since the type is specified for them.
{
"foods": [
{
"type": "apple",
"apple": {
"name": "Matt's Apple",
"variety": "Pink Lady"
}
},
{
"type": "taco",
"taco:": {
"name": "Nel's Taco",
"spiciness": "medium",
"condiments": [
"avocado",
"pico de gallo"
]
}
}
]
}
Support Future Enum Values
It might not be obvious at first, but adding a new enum value to an API spec, unlike adding a new model attribute, is a backwards-incompatible change. The new value won’t be recognized by existing apps, and they won’t automatically know what to do when they encounter it.
To protect released versions of a mobile app from breaking when a new enum value is introduced, we explicitly define a default value that clients can fall back to when they encounter an unrecognized enum value.
In lieu of an obvious default, it’s helpful to define a literal unknown value. Even though the server might never return the value unknown
, this facilitates two things:
- We can define client behavior for the unknown state directly in the API spec and eliminate ambiguity about how clients should respond to future unknown values.
- Developers consuming the API must explicitly handle unknown enum values right from the beginning, so every version of the app will gracefully support future unknown values.
A Common Theme
These tips share a common theme: they help us embrace change. Digital products are dynamic, evolving things–not one-time efforts that end with initial public release. You can ship updates more easily and make end users’ experiences better by considering backwards compatibility, anticipating hotfixes, and building a foundation for your API that can accommodate the new requirements of a product as it evolves.
Sam keeps things spicy with his liberal definition of condiments at Livefront .