While a cup of coffee may seem like its two parts of hot water and coffee
grinds, there is much more to it. The same can be said for a well-designed API. A good API does more than just taking input and returning output. It offers a few key items that make it enjoyable and easy to work with.
These few key items can sometimes be overlooked or not get the extra polish they deserve which detract from the API. Below we’ll touch upon these and how they can enhance the API experience.
The first interaction any developer will have with an API is its documentation. Good documentation is informative and clear which allows developers to quickly start integrating with the API.
Let’s mockup what snippet of what our documentation might look like for a call that allows us to create a coffee bean.
Create Coffee Bean
Description :This will create a coffee bean resource
Endpoint : /coffee-beans
Method: Post
Request Body Schema: application/json
Fields :
type string (required) : Type of coffee bean
region string (required) : Region from where coffee is from
roast string (required) : Type of roast for this coffee bean
limited boolean (optional) : If this bean is a limited batch
quantity int (optional) : Available bag quantity to purchase
Request example
{
"type" : "arabica",
"region" : "brazil",
"roast": : "dark",
"limited" : true,
"quantity": 10
}
Response:HTTP code: 201
Content/Type: application/json
Response example:
{
"coffee_bean": {
"id": "8ed6b848-150f-490e-903c-e6f6042dbe6b",
"type": "arabica",
"region: "brazil",
"roast": "dark",
"limited": true,
"quantity": 10
}
}
Let's dissect each part of the documentation.
First, it starts with a clear name for this API call Create Coffee Bean. Then it is followed up with a description, endpoint, what format the request should be in, and the HTTP method. This gives us enough information to understand the purpose of this call and how we may want to use it.
This is a great starting off point if this is all of our create calls did. If there are available query parameters or specific authentication headers that need to be set these should be documented.
Second, the fields section outlines what should and shouldn’t have to be set in the request call along with their respective types. We also do know
this request has to be sent as a JSON thanks to the first section of the documentation.
Below fields, we show a
example request
…while maybe not necessary it may be useful to provide an example to clear up any confusion one might have.Third, the Response section informs us what kind of response(s) we could expect from the call. In the example above a create call will return a 201 with a
coffee_bean
resource that confirms our request input was valid. The response returns all of the information we provided in addition to a unique ID for this coffee bean.Now while this response section is clear as to what you can expect for
calls that work what if a call fails? If we had authentication requirements for the create call and those credentials were not supplied then a 401 response could be returned.
Returning all possible responses good or bad makes it clear what kind of behavior one would expect to encounter with the API…it offers consistency. It also always for developers to building in proper error handling within their integration with the API.
A URI, Uniform Resource Identifiers, is which resource your API path is modeling.
Let's take our
coffee-bean
example. This API exposes a resource of a coffee-bean
as we know it has a type, a region, a roast, etc so we know that can we manipulate a coffee-bean
. That being said our URI should model the resource, not an action — we do not want calls such as /create-coffee-bean
. Our documentation will then address in what ways you can manipulate said resource.Below is an example of how you could manipulate the coffee-bean resource.
POST /coffee-beans Create Coffee Bean
GET /coffee-beans List all coffee beans
GET /coffee-beans/{id} Get specific coffee bean based on ID
Patch /coffee-beans/{id} Update specific coffee bean based on ID
Delete /coffee-beans/{id} Delete specific coffee bean based on ID
You will notice that our
coffee-bean
the resource is plural. The rule of thumb here is resources should be pluralized unless they are a singleton resource. Another note URIs should always be lowercase and dashes -
should be used instead of underscores _
.No matter what calls we are making on a resource, the types from our requests and responses should be consistent.
Let's revisit the
create coffee bean
call we know that the field quantity is of int
. This int type should be consistent in the request/response across all calls on the coffee bean resource. While it may seem innocent and minor to have the quantity
be returned as string
in another call, this can lead to possible issues and bugs for developers interacting with your API.Another consistency that should be achieved is in the returns top-level node. Let's take two calls Get Coffee Bean and List Coffee Beans. These will return two different resources get will return a single coffee bean while the list will return many. So the responses should look like this:
// Get Coffee Bean
{
"coffee_bean": {}
}
// List Coffee Beans
{
"coffee_beans": [{},{}]
}
Looking at these responses you can easily and quickly determine what resource was called. It also extends itself to easier integrations a developer
can model an object and easily unmarshal these JSON responses.
As more and more users interact with your API the amount of data they are storing will also grow. While this may not seem like a problem at first,
let's imagine that a user has created over 500 different coffee beans
using our API.
This can put a heavy load on our API and not to mention it is a very large
payload to return. Having your APIs list calls be paginated will help
alleviate these issues and still allow for users to use your API without having to worry if their List request will time out or take too long.
While we never want an API to not work properly or throw errors, it is bound to happen. This is why an API should return consistent and informative errors.
Here we have a sample error response.
{
"error": {
"status_code": 401,
"message": "unable to authorize request",
"error_code": "er401auth",
"resource": "coffee-bean",
}
}
Here our errors will always have a top-level node of
error
and will always return the HTTP status code, a message explaining what has occurred, a unique error code, and which resource this happens in.The benefit to having all errors be returned as JSON like this is as a developer this gives me all the information I may want for logging and error handling on the integration side. With error responses like this, the integration can easily build in error checking and not worry that the API might throw an unknown error.
While each of these key items plays a role in having a good API there is
one similarity across all of them. That similarity is consistency. Without consistency in an API, it will be difficult to work with and not offer a stable integration.
There are still a few more recommendations I would like to share.
JSON API Specification
If you are designing your resources JSON structure I would recommend following a JSON specification. This will have your APIs JSON structure stay consistent with other public APIs and not throw developers for a loop when you use a JSON schema you came up with.
OpenAPI Specification/Documentation
The OpenAPI Specification (OAS) is a Linux Foundation Collaborative Project aimed to define standards for language-agnostic interface for HTTP
APIs. If you model your API to a validate OAS yaml/json you can then use this to generate documentation. There are various tools that make this easy the two I recommend would be Swagger or Stoplight.
There are also
Also published at https://medium.com/swlh/what-makes-a-good-api-7126d1630f96