In today’s post we will look at how we can avoid some common pitfalls in API design. The examples will be presented in Ruby on Rails code, but the main takeaways can easily be applied to any decent web framework.
We’ve all been here: the data model has been designed, and we have the entities and relationships implemented. It is finally time to code up the business logic and we are faced with solving these problems:
The actions we want to implement on a particular model are not part of the good old CRUD (Create, Read, Update, Delete) operations, and we need to make a lot of non-standard controller actions, and routes.
The actions we want to implement do not belong to a any model, and we have to create a new controller just for the “API”.
Let’s consider an application like a website analytics portal. For the sake of simplicity, let’s say that we have only 2 models:
Visit. For each
Site we accumulate information about individual visits. The
Visit model stores all the usual things like:
- Time of visit
- Current page
- Previous page
We want to generate various types of reports and visualizations based on the
Visit data for a particular
Site. This includes things like: graphs, pie charts, funnel analysis, etc…
Let’s try the naive solutions, and see why they are not ideal:
First thought: let’s add new actions to the
This may sound like a good idea, because essentially we will just take a bunch of
belong_to a particular
Site and then draw a graph based on that data.
However, here are some problems with this approach:
- It is not the responsibility of the
SitesControllerto know the specifics of drawing various types of graphs.
- The reports we want to generate are likely to change very often, while the standard CRUD operations on the
Sitemodel are very stable. Coupling code that changes frequently with the code that is not expected to change is always a BAD idea.
Second thought: let’s add a new controller!
This would decouple the reports generating code from the
SitesController, which greatly increases the maintainability of our code, and is in line with the Single Responsibility Principle.
But we can still do better, and here are some of the reasons why:
- Adding a new controller just for the API means that we will have many non-standard routes, and soon enough we will end up with a mess no one would dare to change in the future.
- We would have to roll our own validation and error reporting system for each endpoint.
- Testing becomes harder, because all the logic for generating the reports and visualizations is coupled with the code that handles the client requests and sessions, and these two have nothing in common.
The most commonly applied design pattern in this situation is the Command pattern. Basically it suggests wrapping each request into a separate object - whose responsibility is to handle it.
Let’s see how this would look like in our example:
- We introduce a new model called
Report. It will hold information like:
- Type of report:
- Which properties of the
Visitobject we want to include in the report
- Date range
- Particular OS
- Filtering, sorting
- Type of report:
- Whenever we want to get a new graph drawn, or analysis performed, we actually issue a
POSTrequest to the
ReportsControllerand simply create a new
Reportobject with the passed in parameters.
- We can implement the logic for generating the reports inside the
Reportmodel. It just makes sense, when you think about it!
- We can quickly
scaffoldout the views and API endpoints for this.
- We can use the provided validation framework of
ActiveRecordto make sure that the user is requesting a report that can actually be generated. And provide meaningful error messages otherwise.
- We can extend the reporting capabilities of our application without changing our controllers or routes.
Reportmodel is not coupled to any other part of the system, so unit testing becomes really easy.
- Every report API request is logged, and we can get usage statistics, reproduce bugs, etc. which is very important, once our code hits production.