At Fabernovel, when we write a REST API, we want to synchronize our tests with json schemas from our
documentation, we want to write interactors for each CRUD operation and we also want to write our
resources inside a namespace. Doing these extra steps greatly increases the readability and the maintainability
of our code base. But it also increases the boilerplate a developer needs to write.
Inheritance at the rescue?
Inheritance can be used as a refactoring tool and is a way to avoid code duplication. For example,
Active Admin uses the gem inherited_resources.
It allows us to create a CRUD controller by simply inheriting a base controller.
It is very simple, it follows the Convention over Configuration principle and is definitely not boilerplate.
However, there is a problem with this solution: how do you customize the behaviour ? What methods should you override ?
For example, how do you solve these use cases:
You need to transform the request before creating your resource like converting an amount with included taxes to a tax-free amount.
You need to send an email after creating a resource or when an error occurred
You need to perform a payment when creating a resource
You need to restrict access and only respond with a subset of the current resource like delivering only the user’s projects.
There are solutions but if you need to look at the documentation from inherited_resources then you increased
the entry barrier.
It is also easy to misuse this gem. Do you know when to override collection or end_of_association_chain ?
Do you know when to override create_resource or use create! ? It’s easy to find the answers in the documentation but
if the code is already in in your project, you will reuse the current method without knowing if there are
better ways to solve your problem.
There is another flaw in this gem: we are writing fat controllers and are reducing testability and reusability. We know that
we can solve this problem with interactors. But how can we avoid writing the boilerplate code introduced by it ?
Code generation
Rails is shipped with a generator tool built on top of Thor. It allow us to use a cli
to generate source code. For example, rails g model Project will create
We can use this tool to create our own generator that will meet our goals when it comes to architecture. We can rely on the current database
schema to write code that we would have written by hand. Generating code solves our problem of having to write boilerplate.
This first step is to create our generator: rails g generator clean_code
Model
Then, we need to create two templates to create our model and the associated test. Our architecture is based
on namespaces to reflect the Screaming Architecture, so
we copy our files in a domains folder.
The generated model will look like this:
For the model, we use database constraints to generate validations and foreign key to generatebelongs_to/has_many relations.
By generating the code and not writing it, we avoid typos on relation names, we don’t forget to set inverse_of.
In case we had forgotten, we think beforehand of what we should do with has_many relations if we try to destroy our object.
Finally, the generated test is here to ensure validations are correctly set.
That kind of test seems a bit dumb but there are two reasons we create them. First, we need a placeholder. If we want to add
a test, it is easier to do it when the test class already exists. And secondly, for every feature we want a test. In a perfect
world, if we mutate our code, a test should fail. We don’t test the framework, we are testing that the validations are applied.
Rails validations behaviour can change. If you did the upgrade of Rails to 5.0,
you know the required: true is now the default option for belongs_to. Doing this upgrade (and probably future upgrades)
is a lot easier with a good test suite.
Templates code
Controller
We do the same for our controller which contains the majority of the boilerplate.
Other actions like show, update, destroy are also generated but for the sake of simplicity they are omitted. Our controllers’
mission is just to handle the http requests and responses. All the business logic is done in our interactors
(GetProjects and CreateProject, here). The contract of our interactors is that they all have a method call with
keywords arguments and return a monad (See dry-monads ). If the interactor action is
a success we simply render our project and if it is an error then we render the error using its error code. We assume the controller uses devise
and provides a current_user as more than 80% of our controllers require authentication.
The generated test looks like every rails controller test except for the validate_json part.
Using our internal documentation tool Pericles, we can download and versionize the json schemas describing all http requests and responses for our project in our repository.
Hence, in our tests when we perform the request or receive a response, we can verify that they match the specification that we got from our documentation. Using a task, we automatically download the
schemas and save them to our VCS. Using the schemas in our tests allows us to keep the documentation up to date.
When we run our generator, the REST endpoints that we added to our project are also automatically added to the documentation.
Templates codeDocumentation creation
Interactors
Our interactors are straightforward and the associated tests do nothing that you would not expect.
We use an @authorizer that is a simple adapter around the Pundit interface. We use Ransack
for filtering and sorting and Kaminari for pagination. We don’t always use pagination but having it opt-in by default
is a good reminder to think about it. Since our interactors are plain ruby objects, it is very easy for new
developers to edit the current behaviour.
Templates code
Serializer, Policies, Factories
Serializers, policies and factories are generated with all attributes. It is up to the developer to remove some if required. We find
it is a lot easier to remove attributes than to add them as you cannot add typos when removing attributes.
Templates code
Conclusion
We chose to use json schemas, interactors and domains to write quality code and maintainable Rails applications.
By using code generation to bootstrap a REST API, we don’t need to write all the boilerplate resulting from our (architectural) decisions and can
focus on the business logic of our app.