Here at RestCase we are working with many companies and help them develop secure services and APIs. When working with developers on how to design and build quality APIs and microservices and I am seeing many common design problems that are not organization / company-specific. These problems are better to be addressed at the design phase since it will make your API consistent, more readable and robust.
Here are the top 5 issues I am seeing when it comes to designing an API:
Problem 1: Using body in GET requests
In GET requests, add parameters inside the body, instead of in a URL.
Many servers might not even look at the body of the request when it comes to GET method, since the RFC7231 states that:
A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request.
You are using HTTP GET method for "writing" while your URIs do not identify a resource but some operation you would like to perform on the resource. HTTP GET has been designed to be idempotent and safe, meaning that no matter how many times you call GET on the same resource, the response should always be the same and no change in application state should occur.
Yes. In other words, any HTTP request message is allowed to contain a message body, and thus must parse messages with that in mind. Server semantics for GET, however, are restricted such that a body, if any, has no semantic meaning to the request. The requirements on parsing are separate from the requirements on method semantics.
So, yes, you can send a body with GET, and no, it is never useful to do so. This is part of the layered design of HTTP/1.1 that will become clear again once the spec is partitioned (work in progress).
Problem 2: Usage of wrong or lengthy names for parameters
Some developers still think that it is better to give a precise parameter name that states the resource, like project_id. When working with REST APIs, try to follow the best practices for URI design.
projectid -> id
projectname -> name
err_msg -> message
Since the URI already should state what is the resource, like project, there is no need for the project_id, but only id.
Also, if you are using lengthy names like "pagenum", first better use a dash(-), instead of an underline("") like "page-num" and secondly, try to avoid it! Just add a description of the parameter in your documentation:
page | defines the page number to search.
Problem 3: Define own error code and error message.
Error handling in many APIs 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 the presence of error or error code fields. There is a "problem details response"
which is well-defined by the RFC7807 standard that actually defines the interface of a correct and well-known error response. Unfortunately, it is not used too much since not many developers are aware of it. I wrote about it in [blog post and really recommend everyone to read it.
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.
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.
Try to reuse the HTTP protocol first :)
Problem 4: Ignoring caching.
It is easy to ignore the caching by including a header "Cache-control: no-cache" in responses of your API calls. HTTP defines a powerful caching mechanism that includes ETag header, If-Modified-Since header, and 304 Not Modified response code. They allow your clients and servers to negotiate always a fresh copy of the resource and through caching or proxy servers increase your application's scalability and performance.
ETag (entity tag) response header provides a mechanism to cache unchanged resources. Its value is an identifier that represents a specific version of the resource.
How it works?
Followings are the generally high-level steps where response header 'ETag' along with conditional request header 'If-None-Match' is used to cache the resource copy in the client browser:
The server receives a normal HTTP request for a particular resource, say project with id=123 to get the project details.
The server prepares the response but in order to help the browser with caching (By default all browsers always cache the resources (specification) so no special header in the response is needed) and includes the header 'ETag' with its value in the response:
The server sends the response with the above header, the content of project 123 in the body and with the status code 200. The browser renders the resource and at the same time caches the resource copy along with header information.
Later the same browser makes another request for the same resource project 123 but with following conditional request header:
On receiving the request for project 123 along with 'If-None-Match' header, the server logic checks whether project 123 needs a new copy of the resource by comparing the current value of the ETag identifier generated on the content of project 123 (or saved in some other place, which is useful when the content is very big in order not to calculate the ETag again) and the one which is received in the request header.
If the request's If-None-Match is the same as the currently generated/assigned value of ETag on the server, then status code 304 (Not Modified) with the empty body is sent back and the browser that uses a cached copy of project 123.
If the request's If-None-Match value doesn't match the currently generated/assigned value of ETag (say "version2") for project 123 then the server sends back the new content in the body along with status code 200. The 'ETag' header with the new value is also included in the response. The browser uses the new project 123 and updates its cache with the new data.
This is very useful and gives several advantages like saving network bandwidth since this the server does not return content body but relies on the browser for that. This also speeds up the communication between client and server and eventually increases your application security, scalability, and performance.
Problem 5: Returning too much data and long response times.
When you start building your REST API, don't forget that the resources you are returning can increase both in the count and in size. With time, this can cause your microservices to be under increased load and increase the response times of your REST API.
Nobody wants to call an API and get a response after 2 minutes.
Start designing your REST APIs with support for pagination, sorting and filtering.
URL parameters are the easiest way to add basic filtering to REST APIs. If you have an /projects endpoint which lists school projects, you can filter via the property name such as GET /projects?state=active or GET /projects?state=active&student=1234. However, this only works for exact matches. What if you want to do a range such as an age or a date range?
The problem is URL parameters only have a key and a value but filters are composed of three components:
The property or field name
The operator such as eq, lte, gte
The filter value
There are various ways to encode three components into URL param key/values.
List all the possible options for filtering in your API documentation and enforce strong validation on the inputs like checking if this is a valid number, valid date and etc...
Most endpoints that return a list of entities will need to have some sort of pagination.
Without pagination, a simple search could return millions or even billions of hits causing extraneous network traffic.
Paging requires an implied ordering. By default, this may be the item’s unique identifier but can be other ordered fields such as a created date.
Limit/Page Paging would look like GET /projects?limit=20&page=5. This query would return the 20 rows starting with the 100th row (5 pages of 20 items). Not sending the page, will default it to 0.
Like filtering, sorting is an important feature for any API endpoint that returns a lot of data. If you’re returning a list of users, your API users may want to sort by last modified date or by email.
To enable sorting, many APIs add a sort or sort_by URL parameter that can take a field name as the value.
However, good API designs give the flexibility to specify ascending or descending order. Like filters, specifying the order requires encoding three components into a key/value pair.