Rails ApiController and Concerns
Hi there! Today I want to share with you some code architecture decisions I’ve made during the development of rails-api-base project. This post is about how I’ve structured the code that concerns about controllers common behavior (I’ll only speak about controllers, but the concept can be applied to models too).
ApiController versioning
The DRY (don’t repite yourself) basic principle
forces us to collect all spread common code of the controllers
and put it inside the ApplicationController.
Nevertheless, when developing an api we must care about code versioning too.
As is normal, almost all of this common code
is related with the input parameters
or the output format and affects directly to the api version contracts.
Because of that, I’ve decided that it needs to be versioned,
like the other controllers.
That’s the reason to create ApiController,
which has the same purpose as ApplicationController,
but it’s placed under app/controllers/api/v1
folder.
Hence, we’ll use one or the other
depending on if the generic code must be versioned or not.
So finally, the inheritance path of an ordinary controller will look like this:
And this is the file tree of rails-api-base controllers:
ActiveSupport::Concern
But what are those “concerns”?
In order to don’t mess the ApiController
with too much and unrelated code,
it can be structured by independent modules using the
Rails ActiveSupport::Concern
module type.
Each file under /concerns
implements one of these modules,
whom are included in the ApiController, thus, to all api controllers.
Concerns let you define variables, methods…
as usual, but also include behavior to the controller itself
(e.g.: define a before_action
or rescue_from
).
That is all we need to refactor the ApiController.
I’ve applied this pattern to the project and the result is an ApiController as clean and simple as that:
and here you can see an example of one of the concern modules:
The unique remarkable thing is that
the module extends from ActiveSupport::Concern
and places the controller specific code, such as an before_action
,
under included
(the filters, rescues… code that only works from inside a controller class).
On this post you can read more about the four modules included:
Concern modules testing
Finally, last but not least: concern modules testing using rspec.
- How do we test a controller module that affects all controllers?
- We must define a fake controller that inherits from ApiController and check that the behavior included by the target module works as expected.
Here is an example:
Here we can see how the fake controller is defined adhoc inside the spec.
In this case, I’ve also taken the opportunity to test the current_user
method, apart from the most important concern purpose defined by the
before_action :auth_with_token!
.
Few lines below the controller, appears the also fake and adhoc routing. Usually, the drawn route would be the following one:
but from Rspec version 3 it raises the following error
when the inherited controller is not in the app/controllers/
folder root:
To fix that I’ve changed the route parameters to the controller path
and called it with its name instead of ‘anonymous’
(i.e.: if Api::V1::SomeController
the route is 'api/v1/some#method_name'
):
And that’s it for now. All feedback is welcome!
Here you can take a look at the whole api code -> rails-api-base