Some Techniques for Better Usability in REST API Design
Someone said that API designers are UX engineers for developers. Good API design should be easy to use for consumers as well as easy to be implemented by developers.
Here are some techniques I summarized from my API design tasks. Let me know if it is helpful or you have more ideas on how to design a easy-to-use API.
Return 200 OK with Empty Array or 404 Not Found?
Problem
Sometimes, it is hard to tell if a GET API should return 200 OK with an empty array or it should return 404 not found. Actually, the answer is how the API client would like to handle it.
A 4XX response usually indicate there is something wrong in client’s request. Client also will treat it with a customized error page. Before an API designer makes a decision, it’s better to ask client developers how they will use the response.
Technique
However, there are common practice on which status code we should use. A API should return 200 OK with empty array if the purpose is to search records for a player or other criteria since an empty array isn’t an error caused by the client. It just means there isn’t any records of the player. However, the API should return 404 if the player cannot be found in the system because the client may entered a wrong player ID and the system should prompt this error to the user.
Example
Here is an example. In this example, the system tries to search active user sessions of a station. It returns 200 OK with an empty array while it returns 404 not found if station ID cannot be found.
Use controller or Use PUT to modify a resource?
Problem
In RESTful APIs, PUT is suggested to modify a resource but we also see APIs use controller with a POST for resource modification. Which way is better?
When you use put, you use body to wrap the object to be modified but the issue is we only need to change a status of an object, which is minimal change. You may confuse a developer in such case if you transfer an entire object to service for this purpose.
Technique
Use controller to minimize the number of parameters if you only want to change a status.
Example
You can see the difference between changing status of CPV (chip purchase voucher) with PUT and Controller.
PUT
The following images show the API use put to change a CPV status to redeemed and voided. The request body is CPVInfo object but you can see the only mattered properties are CPV ID, CPV status, and transaction info among all the properties. The API designer needs to tell developers which properties should be used in the API. This lowers usability of the API. The good news is that this make it easier to see transaction details if we need to analyze logs because the request body includes all the detail information of the CPV. We don’t need to reference other information source such as database.
Controller
The following example uses redeem and void controllers to redeem and void a CPV. You can see only transaction information is needed in request body. This makes much clear to API developers what parameters are actually should be used in the implementation. There isn’t CPV related parameters except CPV ID.
Use Read Only to specify properties that only should be shown in a response
Problem
Designers always want to reuse schemas to save their effort. For the above example, a CPVInfo object definition is used in both issue CPV API and redeem/void CPV API but not all properties of CPVInfo object are used in said APIs. Some properties should only be used in request and some properties are supposed to be used in response. How can API designers describe the usage scenario in this case?
Technique
In open API specification, you can mark a property as ReadOnly, which means this property shouldn’t be used in request and it will only be shown in response.
Example
In the following example, I mark CPVInfo.IssuanceTime as ReadOnly since it is generated when a CPV was issued at service side. It’s not required when client tries to issue a CPV. As you can see, it won’t appear in issue CPV API request.
IssuanceTime is marked as ReadOnly in CPVInfo schema.
Use Problem Details to show errors to client
Problem
For API errors, we often see different products use different error object definition. Actually, there is an industry standard for RESTful API error object definition. It is also recommended in REST API guideline and adopted by .Net Core.
Technique
Use ProblemDetails to describe API errors. It is defined in https://tools.ietf.org/html/rfc7807. It’s easier for your API consumer to understand and use your error description using industry standard. They can easily find documents of it online.
Example
Use sub path for sub transaction types
Problem
We need to use many words to document an API usage if this API has a few special cases or sub types. In most cases, our user cannot use the API correctly without reading the document. For PA engineers, it also takes time to test all the special cases or sub types if they are packaged in one API. Any modification to the API causes a full regression.
Technique
We can use sub path for special cases or sub types under the API so that user can learn all these cases and sub types by just reading the API path and testers can test the API cases one by one without worrying a chained error effect when there is a modification.
Example
Zero Play Rating
The following request is used to add a table rating to CMS. Users need to fill quite a few parameters to post a rating. This works in most cases but there is a special case which is that a player doesn’t play. We call it Zero Play. To post a Zero Play rating, users need to figure out Zero Play parameters pattern, which will take some time.
The solution to it is to add a sub path to the existing Post Rating API. For this example, we will change /api/v1/table-ratings to /api/v1/table-ratings/zero-play and remove the unnecessary parameters from the request. In this way, users can know how to post a Zero Play rating just from the API path.
{
"transactionInfo": {
"transactionTime": "2020-07-08T01:18:07.0623569+00:00",
"postedBy": "*****",
"verifiedBy": "*****",
"enteredBy": "******",
"workstation": "123"
},
"ratingId": 38020,
"sourceId": "1",
"sourceName": "test table",
"playerId": 125,
"programId": 0,
"tableID": "LA01BJ01",
"gameType": "BJ",
"gameName": "BlackJackBJ",
"gamingDate": "2020-07-08T01:18:07.0623888+00:00",
"shift": "1",
"pitID": 1,
"pitName": "Poker 1",
"position": 2,
"row": 1,
"startTime": "2020-07-08T01:16:07.0624008+00:00",
"endTime": "2020-07-08T01:18:07.06241+00:00",
"timePlayed": 500,
"averageBet": 544,
"speed": 49,
"handsPlayed": 6,
"cashIn": 3951,
"chipsIn": 1078,
"markersIn": 1287,
"moneyPlays": 1424,
"chipsOut": 2908,
"ratingIns": [
{
"code": "1",
"description": "bla in",
"amount": 2528,
"cashEquivalent": false,
"displayOrdinal": 0
},
{
"code": "CashIn",
"description": "Cash In",
"amount": 15255,
"cashEquivalent": false,
"displayOrdinal": 0
},
{
"code": "CashlessIn",
"description": "Cashless In",
"amount": 2589,
"cashEquivalent": false,
"displayOrdinal": 0
}
],
"ratingOuts": [
{
"code": "2",
"description": "bla out",
"amount": 1766,
"cashEquivalent": false,
"displayOrdinal": 0
},
{
"code": "ChipsOut",
"description": "Chips Out",
"amount": 6582,
"cashEquivalent": false,
"displayOrdinal": 0
},
{
"code": "CPVRedeem",
"description": "CPV Redeem",
"amount": 5430,
"cashEquivalent": false,
"displayOrdinal": 0
}
],
"netBuyIn": 390,
"winLoss": 743,
"skill": "Average",
"ratingComments": "test comment",
"skillFactor": 4,
"dealer": "******",
"marker": [
{
"documentId": 198273,
"amount": 10
}
]
}