Ruby on Rails Projects
Project Setupβ
RULE #1: always start a new project with a fresh new installation of rails with the latest framework and libraries. If the project is similar to a previous one, still do not clone it! In this second scenario, after creating a new project with everything up to date you can copy and paste single functionalities if needed; it would be better to rethink everything. In this way, you can find useless dependencies, dead code, code smells, and pitfalls..
RULE #2: if your last Rails project was a v5 and now you are starting a v6, first of all read again the Rails guides https://guides.rubyonrails.org/ , expecially sections related to Models and Other Components (ActiveSupport and ActiveModel)
Start thinking from day 1 if the Rails project will be only an API and/or will use some frontend framework (React, Vue)
Must to have gems and librariesβ
Below list of the mandatory gems that must be used every time:
- Devise for authentication
- Pundit or cancancan for authorization
- sidekiq for background jobs
- redis for caching
Other gems best practices:
- Do not add gems that are not actively maintained or with a few stars
- Do not add unknown gems just to reinvent a little wheel: see what they do and create a library.rb inside the lib folder
- Add the minimum number of gems to your project: more dependencies mean more issues in the future if you have to upgrade the project
- If you plan to add react inside the application, REMOVE turbolink support
- Take a look at https://www.ruby-toolbox.com/categories to check trends and choose the specific library for you needs
Service Objects?β
Move away from fat models and _skinny controllers and use service objects (or interactors).
A service object is a Ruby object that performs a single action. It encapsulates a process in your domain or business logic.
Rails apps tend to start simple, with clean models and controllers. Then you start adding features. Before you know it, your models and controllers are big, unwieldy, and hard to understand. Refactoring into service objects is a great way to split these big pieces up, so they're easier to understand, test, and maintain.
As your application grows, you may begin to see domain/business logic littered across the models and the controller. Such logics do not belong to either the controller or the model, so they make the code difficult to re-use and maintain. A Rails service object is a pattern that can help you separate business logic from controllers and models, enabling the models to be simply data layers and the controller entry point to your API.
We get a lot of benefits when we introduce services to encapsulate business logic, including the following:
Lean Rails controller - The controller is only responsible for understanding requests and turning the request params, sessions, and cookies into arguments that are passed into the service object to act. The controller then redirects or renders according to the service response. Even in large applications, controller actions using service objects are usually not more than 10 lines of code.
Testable controllers - Since the controllers are lean and serve as collaborators to the service, it becomes really easy to test, as we can only check whether certain methods within the controller are called when a certain action occurs.
Ability to test business process in isolation - Services are easy and fast to test since they are small Ruby objects that have been separated from their environment. We can easily stub all collaborators and only check whether certain steps are performed within our service.
Reusable services - Service objects can be called by controllers, other service objects, queued jobs, etc.
Separation between the framework and business domain - Rails controllers only see services and interact with the domain object using them. This decrease in coupling makes scalability easier, especially when you want to move from a monolith to a microservice. Your services can easily be extracted and moved to a new service with minimal modification.
If a service object does more than one specific action, we'll call it orchestrator: for example an orchestrator encapsulates logic for:
- creating a user
- creating his profile
- send him a welcome email
The most important thing is to create service objects that ARE NOT DEPENDENT from the request: you must pass them clear parameters (like strong_parameters) but not the entire request object.
The service object must not check authorizations and users, if someone calls it, it performs the business logic without asking why.
Some notable gems for service objects:
- Service Objects: https://github.com/manuelmeurer/services
- Active Interaction: https://github.com/AaronLasseigne/active_interaction
- Simple Command: https://github.com/devinterface/simple_command
Testingβ
RULE #1: Setup automated tests ONLY if you are sure that for all application lifetime you'll maintain them. Otherwise it's time wasted.
RULE #2: Setup automated tests ONLY if you have budget and time to keep them in sync: a good codebase costs 30% of project time.
Once you are really sure you want to add automated tests, do not use external libraries and use the Rails built in test suite: https://guides.rubyonrails.org/testing.html
If you really don't like the built in library, use RSPEC: https://rspec.info/
Use factories to genrate dummy data into the database: https://github.com/thoughtbot/factory_bot
WHAT TO TEST
RULEΒ #1: Do not test everything, don't lose time testing first (unless you are a really fun). Test what means to be tested, like:
- difficult calculations
- complex features
- service objects behaviour
RULE #2: Avoid mocking and stubbing requests because your tests will probably pass also if the external APIs for example has changed their response.
RULE #3: Avoid mocking database: create less tests but save data into database: remember to cleanup everything at the end of each test.
RULE #4: As per #3, do not generate dependent tests: they should be run randomly and they must pass all the time.
RULE #5: forget about code coverage: test what really means to be tested, sometimes fixing the code is less expensive than creating huge test suites
Lintingβ
A good linter must be added as dependency and run on save and on type. Editors like VSCode alreasy do that.
Rubocop https://rubocop.org/ is the best choiche, expecially with the addion of Shopify style guide: https://ruby-style-guide.shopify.dev/
Add them bot to the Gemfile
To run rubocop before a commit, add a pre-commit hook: https://oozou.com/til/run-rubocop-before-commit-8
Databasesβ
Postgres should be the first choice if you need a relational database; if you are planning to use MongoDB, before starting a rails project, consider moving to Node.
If you use postgres, enable uuid extension: ```CREATE EXTENSION "uuid-ossp";````
In this way you avoid guessable routes like ```/posts/1/edit````
This doesn't mean you are exempted to check authorizations in the controller!
If you still need to work with MongoDB, the go to choice is MongoId: https://docs.mongodb.com/mongoid/current/
Writing API?β
If you know you are writing an API only, first take a look at https://guides.rubyonrails.org/api_app.html and then create the rails project with the command rails new my_api --api.
Rails lacks a good way to serve complex JSON payloads, so you can use libraries like https://github.com/jsonapi-serializer/jsonapi-serializer or the built in https://github.com/rails/jbuilder
Useful tipsβ
TDB