Smelly REST API: Measure Project Quality by Reviewing API Endpoints

REST API as an indicator of the project quality.

Sasha Marfut
Level Up Coding

--

Photo by Kenny Eliason on Unsplash

Imagine you are browsing the Swagger API endpoints of some web service that is new to you:

You can see the request and response models of each API endpoint, their HTTP methods. In addition, you can execute any endpoint you want.

After browsing API endpoints for a while, how much do you think you can find out about project architecture, maintainability, health and other stuff?

In fact, API contracts and their basic behavior can tell you a lot of things, and in this article, we’re going to find out what those things might be.

Lack of HATEOAS

HATEOAS stands for Hypermedia as the Engine of Application State.

Simply put, HATEOAS is a principle that suggests including hypermedia links into API responses to guide the clients on what action they can take next. Thus, the backend has complete or near-complete control over the application flow.

Without using HATEOAS, business logic can leak to the client side. This is especially bad when many different clients use the same API, because each client will have to implement the same logic.

Let’s look at a simple example.

Imagine we are developing an API that needs to manage the workflow of a article entity that can be in one of the following states:

  • Draft
  • Pending Approval
  • Published

The client, which for example is the front-end, must be able to submit a request to change the state of an article entity. To do this, the backend can expose a simple API endpoint that accepts an article ID and new state:

Request:

{
"articleId": 41,
"state": "PendingApproval"
}

Response:

204 No Content

Now clients can update the state of an article entity whenever they need to.

The main disadvantage of this approach is that the client is not aware of possible workflow transitions (for example, is it possible to immediately move an article from Draft to Published state?). But for the client, this knowledge is essential to, for example, display to the user only states to which they can move from the current state.

Of course, the developer can find all possible workflow transitions in the API documentation and code all the rules in the client.

However, this will lead to what was mentioned at the beginning — leaking of workflow logic to the client side.

HATEOAS can help consolidate workflow logic into one place, which is the backend. An API request that changes the state of an entity should return an array of links indicating which transitions are possible for given state:


Request:

{
"articleId": 41,
"state": "PendingApproval"
}

Response:

{
"articleId": 41,
"links": {
{
"description": "Publish",
"href": "/api/articles/41/publish",
"method": "PATCH",
"rel": "next",
},
{
"description": "Reject",
"href": "/api/articles/41/reject",
"method": "PATCH",
"rel": "next"
}
}
}

Note that the HATEOAS standard defines 3 properties (href, method, rel) that a link should have, but you can also extend it with your own properties
(e.g.
description).

In the above example, after the client calls the API to move an article to the Pending Approval state, it will receive in response information about next allowed transitions. Thanks to this approach, the client will not have to implement part of the workflow transition logic on their own. Also, the client will not need to construct links, which reduces the likelihood of errors.

Long Response Time

Let’s imagine you execute the GET /article/{articleId} endpoint and it takes 10 seconds to complete. Such a long API execution time to retrieve an entity from the database likely signals some kind of problem in the app.

At best, the cause of poor performance is an inefficient implementation of the flow that extracts the article entity.

Some common causes include the following:

  • A missing index on the database table.
  • The backend is loading much more article-related data from the database than the API should return.
  • The business logic uses a suboptimal data structure when preparing a response (e.g., a List instead of a Dictionary) for a large dataset.
  • Lack of response compression.

These problems can usually be found and fixed quickly.

However, in the worst case scenario, poor API performance can indicate an architectural problem. For example, data that should be stored in one place is scattered across different locations:

Therefore, it takes some time to collect the data from several data sources.

Another reason for poor performance could be the lack of a cache between the service and the data source:

Typically, solving such problems takes longer because it may require partial redesign of big components of the application.

Sync vs Async

Also, the poor performance of the API may be due to the fact that it is implemented synchronously, while it should be asynchronous. And it also has to do with architectural issues.

Here’s a simple example — synchronous service-to-service integration instead of messaging:

Service 1 sent a notification to Service 2 via a REST API call. However, this notification can be implemented asynchronously via a service bus. This can make Service 1 more productive because it does not have to wait for Service 2 to respond.

Inconsistent Error Response Formats

The HTTP protocol defines many different status codes.

Status codes starting at 400 and above are used to communicate to the client that something has went wrong during the execution of the request.

However, returning a status code alone is usually not enough. For example, if you only return a status code of 400 (Bad Request), the client won’t know what is wrong with their request. Invalid request parameter? Which one?

Therefore, to indicate to the client the cause of the failure, developers add an error message to the response. And that’s where things can get messy. I’ve seen cases where the error format responses differ for API endpoints even within the same microservice, let alone different microservices:

//Single error message in the response
{
"error": "Article not found"
}

//Or an array of error messages in the response
{
"errors": [
"Article not found"
]
}

//Or an array of error messages with some additional fields in the response
{
"errors": [
{
"message": "Article not found",
"details": "It could be inactive or archived."
}
]
}

Needless to say, this inconsistency in error responses format can be very frustrating for API clients because they can’t handle failures in a uniform way. Moreover, different error responses can lead to a set of other issues related to maintaining documentation, API testing, integration and so on.

Fortunately, there is a solution to the problem, called Problem Details.

This is a specification that allows you to standardize the format of error responses. Here’s what the Problem Details response looks like:

{
"type": "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 404,
"instance": "/articles/41/",
"traceId": "0HN1H8H3KJAC1:00000003",
"errors": [
{
"name": "article.not.found",
"reason": "Article not found."
}
]
}

Problem Details responses can be easily turned on into ASP.NET Core or another web framework you use. Once it is enabled, you can simply convert domain errors into a problem details response in the middleware.

Lack of Monitoring Endpoints

There are some essential endpoints that are relevant for most web services.

Some examples:

  • /health: an endpoint the check the health status of the service and its dependencies.
  • /version: an endpoint that returns information such as the release number.
  • /metrics: an endpoint for providing various metrics about the service.
  • /ping: an endpoint to ping the service.

The absence of these endpoints or their inconsistent implementation in microservices can reduce the observability of the system.

Miscellaneous

  • Obscure or inconsistent naming in API request and response models (for example, in one API it’s /GET blog-post/{id}, in another it’s POST /article), in addition to confusing the API clients, also probably means that there are similar naming issues throughout the entire codebase.
  • Many API endpoint versions may indicate that the development team is not devoting enough time to the API Design process (ADDR) that involves such stages as understanding API goals, identifying API boundaries, modeling API profiles etc. Thus, the team should implement a second version of the API to avoid breaking changes instead of implementing the API correctly from the beginning. However, supporting multiple versions may negatively affect the maintainability of the project.

In this article, we looked at some important API smells that typically catch my attention and make me look deeper into the project architecture and its codebase to prove or disprove my concerns.

Thanks for reading. If you liked what you read, check out the story below. Also, please support me on Buy Me a Coffee and follow me on Patreon.

--

--