I was helping out a friend who needed to integrate housing availability from a property management system with his client’s website. Luckily, the property management system had an API. Unfortunately, everything about it was wrong.
The goal of this story is not to give a bad advertisement for a used system, but to share how things should NOT be developed, as well as learn the right approaches when it comes to designing APIs.
Assignment
My friend’s client is using Beds24 system to manage their property listings and keep availability in sync across various booking systems (Booking, AirBnB, etc.). They are building a website and would like the search mechanism to only show properties that are available for the selected dates and number of guests. Sounds like a trivial task, since Beds24 provides an API for integrations with other systems. Unfortunately, the developer has managed to make a lot of mistakes when designing it. Let’s walk through these mistakes, see what exactly went wrong and how it should have been done.
Sin 1: Request body format
Since were are only interested in getting availabilities for properties of the client, only /getAvailabilities
call was of interest to us. Even though this is a call to get availabilities, in fact this is a POST request since the author decided to accept filters as a JSON body. Below is a list of all possible parameters:
{
"checkIn": "20151001",
"lastNight": "20151002",
"checkOut": "20151003",
"roomId": "12345",
"propId": "1234",
"ownerId": "123",
"numAdult": "2",
"numChild": "0",
"offerId": "1",
"voucherCode": "",
"referer": "",
"agent": "",
"ignoreAvail": false,
"propIds": [
1235,
1236
],
"roomIds": [
12347,
12348,
12349
]
}
Let me go through the JSON object and explain what is wrong with its parameters.
- Dates
checkIn
,lastNight
andcheckOut
are formatted using YYYYMMDD scheme. There is absolutely no reason not to use the standard ISO 8601 format (YYYY-MM-DD) when encoding dates as strings, since this is a widely adopted standard understood and expected by most developers and JSON parsers. In addition to that, thelastNight
field seems to be redundant becausecheckOut
is provided and it would always be one day ahead of the last night. Please, always use standard date encoding formats and do not ask the user of the API to supply redundant data. - All Ids, as well as
numAdult
andnumChild
fields are numeric, but are encoded as strings. There seems to be no reason to encode them as strings in this particular situation. - We have the following field pairs:
roomId
androomIds
, as well aspropId
andpropIds
. Not only it is redundant to have theroomId
andpropId
properties sinceroomIds
andpropIds
can be used to pass Ids, but there is also a type issue here. Notice thatroomId
expects a string, whileroomIds
requires a numeric array. This can cause confusion, parsing issues, and would mean that the back-end itself performs some operations on strings and some on numbers, even though we are talking about the same data.
Please, do not confuse developers with such silly mistakes and try to use standard formatting as well as pay attention to redundancy and field types. Do not just wrap everything in a string.
Sin 2: Response body format
As explained in the previous part about request body format, we are only focusing on the /getAvailabilities
call. This time let us have a look at the response body format and see what is so wrong with it. Keep in mind that we are interested in getting the Ids of properties that are available for the given dates and a number of guests. Below are the corresponding request and response bodies:
Request:
{
"checkIn": "20190501",
"checkOut": "20190503",
"ownerId": "25748",
"numAdult": "2",
"numChild": "0"
}
Response:
{
"10328": {
"roomId": "10328",
"propId": "4478",
"roomsavail": "0"
},
"13219": {
"roomId": "13219",
"propId": "5729",
"roomsavail": "0"
},
"14900": {
"roomId": "14900",
"propId": "6779",
"roomsavail": 1
},
"checkIn": "20190501",
"lastNight": "20190502",
"checkOut": "20190503",
"ownerId": 25748,
"numAdult": 2
}
ownerId
andnumAdult
suddenly become numbers in the response as opposed to them being strings in the request body.- There is no list of properties. Instead, properties are top-level objects with
roomId
as keys. This means that in order to get a list of available properties, we would need to iterate over all objects, check if certain parameters likeroomsavail
are present, discard others likecheckIn
,lastNight
, etc. to get a list of properties. Then, we would need to check the value ofroomsavail
property and if it is more than 0, treat the property as available. But wait a second, have a closer look here:"roomsavail": “0”
and here"roomsavail": 1
. See the pattern? If there are no available rooms, the value is a string, while if there is a room free, it turns into a number! This would cause a lot of problems in languages enforcing type safety such as Java, since the same property should not be of different types. Please, use proper JSON lists for displaying a collection of data instead of coming up with weird key-value constructs as the one above, and make sure that you don’t change field types from object to object. A correctly formatted response would look like below (note that this format would allow to also get information about rooms without duplicating anything):
{
"properties": [
{
"id": 4478,
"rooms": [
{
"id": 12328,
"available": false
}
]
},
{
"id": 5729,
"rooms": [
{
"id": 13219,
"available": false
}
]
},
{
"id": 6779,
"rooms": [
{
"id": 14900,
"available": true
}
]
}
],
"checkIn": "2019-05-01",
"lastNight": "2019-05-02",
"checkOut": "2019-05-03",
"ownerId": 25748,
"numAdult": 2
}
Sin 3: Error handling
Error handling in this API is implemented in the following way: all requests return a response code of 200
, even in case of an error. This means that there is no way to distinguish between a successful and unsuccessful response other than parsing the body and checking for presence of error
or errorCode
fields. The API only has 6 error codes, as seen below:
Please, consider not using this approach of returning a response code 200
(success) when something went wrong, unless it is the standard way to go in your API framework. It is a good practice to make use of standard HTTP error codes, which are recognized by most clients and developers. For example, error code 1009 in the screenshot above should be replaced with a 401 (Unauthorized) HTTP code. It makes life easier if the API client could know upfront whether to parse the body or not, and how to parse it (as a data object or error object). In cases where errors are application-specific, returning a 400 (Bad request) or 500 (server error) with an appropriate error message in the response body is preferred.
Whichever error handling strategy is chosen for a given API, just make sure it is consistent and according to the widely adopted HTTP standards. This would make our lives easier.
Sin 4: “Guidelines”
Below are the API use “guidelines” from the documentation:
Please observe the following guidelines when using the API
1. Calls should be designed to send and receive only the minimum required data.
2. Only one API call at a time is allowed, You must wait for the first call to complete before starting the next API call.
3. Multiple calls should be spaced with a few seconds delay between each call.
4. API calls should be used sparingly and kept to the minimum required for reasonable business usage
5. Excessive usage within a 5 minute period will cause your account to be blocked without warning.
6. We reserve the right to disable any access we consider to be making excessive use of the API functions at our complete discretion and without warning.
While points 1, 4 make sense, I can’t agree that others do. Let me explain why.
2. In case you are building a REST API, it is supposed to be stateless, there should be no state present at any point in time. This is one of the reasons why REST is useful in cloud applications. Stateless components can be freely redeployed if something fails, and they can scale according to load changes. Please make sure that when designing a RESTful API, it is really stateless and developers do not need to care about such things as “one request at a time”.
3. This is an ambiguous and very weird guideline. Unfortunately, I could not figure out a reason the author has created this “guideline”, but it gives a feeling that there is some processing done outside of the request itself, therefore making calls right after each other could put the system in some incorrect state. Also, the fact that the author says “a few seconds” provides no specific information about the actual time between the requests.
5 & 6. Again, it is not explained what is “excessive” in this case. Is it 10 requests per second or 1? Also, certain websites would have a huge amount of traffic, and blocking them from using the API just because of that without any warning might make developers move away from such a system. Please, be specific when making such guidelines and think about the users when coming up with rules like these.
Sin 5: Documentation
This is how the API documentation looks like:
The only problems here are readability and overall feel. The same documentation could have looked way better if the author would have used markdown instead of custom non-styled HTML. For the sake of this post, I created a better version with the help of Dilliger in under 2 minutes. Here is the result:
Please, use tools to create API documentation. For simple docs, a markdown file like the one above can be enough, while for larger and more feature-rich documentation, tools like Swagger or Apiary are better to be used.
Here is a link to the Beds24 API docs for those who want to have a look at it themselves.
Sin 6: Security
The API documentation states the following about all of the endpoints:
To use these functions, API access must be allowed in the menu SETTINGS >> ACCOUNT >> ACCOUNT ACCESS.
However, in reality anyone can make a request to fetch data without passing any credentials for certain calls, like get availability of a certain property. This is stated in a different part of the documentation:
Most JSON methods require an API key to access an account. The API code can be set at the menu SETTINGS >> ACCOUNT >> ACCOUNT ACCESS.
In addition to the miscommunication about authentication above, the API key
is actually something that the user needs to create themselves (actually type the key manually, no autogeneration whatsoever) and it should be between 16 and 64 characters long. Allowing the user to create the key themselves could potentially lead to very insecure keys that can be easily guessed, or certain formatting issues since any string can be entered in that field in account settings. In worst cases, it could also become an open door for SQL injection or other types of attacks. Please, never let the user create API keys. Always provide them with immutable autogenerated keys instead, with an ability to invalidate it when needed.
For those requests that are actually authenticated, we have a different problem: authentication token has to be sent as part of the request body as seen below (from documentation):
Having authentication token inside request body means that the server would need to first parse the request body, extract the key, perform authentication, and then decide what to do with the request: execute it or not. In case of a successful authentication, there is no overhead involved as the body would have been parsed anyway. In case authentication failed, the server did all the work described above just to extract the token, spending precious processing time. A better approach instead would be to send an authentication token as a request header, using Bearer authentication scheme or similar. This way, the server would only need to parse the request body in case of a successful authentication. Another reason to use a standard authentication scheme like a Bearer token is simply because most developers are familiar with it.
Sin 7: Performance
Last but not least, it would take on average a little over 1 second for a request to complete. With modern applications, such delays may not be acceptable. Therefore, take performance into account when designing APIs.