Additional Rails conventions for sustainable web development

ยท

7 min read

Maintaining a sustainable codebase is essential to ensure the longevity of your application. However, technical debt, usually incurred from short-term trade-offs, can slow down your team over time.

Tools like cherrypush.com offer you a second chance by helping developers and Engineering Managers stay on top of their technical debt.

In this article, I'll list some of my guidelines for an improved Ruby on Rails codebase and how to enforce them.

Rails Routes

Stick to resourceful routes

โŒ BAD
get "/users/:id", to: "users#show", as: :user
โœ… GOOD
resources :users, only: %i[show]
๐Ÿ’ TRACK IT TO CHERRYPUSH.COM WITH
{
  name: "non-standard rails routes",
  pattern: /\b(get|post|put|delete)\b/,
  include: ["**/routes.rb", "**/routes/*.rb"],
},

In Ruby on Rails, canonical routes refer to the standard, preferred routes that are considered the primary way to access resources within an application.

These routes are designed to provide a consistent and intuitive URL structure, enhancing the usability and maintainability of the application.

Canonical routes often follow RESTful conventions, mapping HTTP methods (GET, POST, PUT, DELETE) to corresponding controller actions for CRUD (Create, Read, Update, Delete) operations on resources.

By adhering to canonical routes, developers ensure a coherent and predictable URL scheme that aligns with industry best practices and simplifies navigation for both users and other developers.

Explicitly whitelist resourceful routes

โŒ BAD
resource :fraud_analysis, except: %i[index new]
โœ… GOOD
resource :fraud_analysis, only: %i[show edit create update destroy]
๐Ÿ’ TRACK IT TO CHERRYPUSH.COM WITH
{
  name: "unscoped resourceful routes",
  pattern: /resources\s:\w*( do)?$/,
  include: ["**/routes.rb", "**/routes/*.rb"],
}

Using only instead of except to whitelist routes on resources in Ruby on Rails provides a more secure and controlled approach to defining routes. When you use only, you explicitly list the routes that you want to include for a resource, effectively blocking all other routes. This follows the principle of least privilege, where you grant only the necessary permissions.

On the other hand, using except might lead to inadvertently exposing routes that you didn't intend to include, potentially causing security vulnerabilities or unexpected behavior. By specifying the allowed routes using only, you ensure that your application adheres to a strict access policy and reduces the surface area for potential security risks.

In summary, using only for whitelisting routes on resources offers better security and control by explicitly defining which routes are accessible and avoiding accidental exposure of sensitive actions.

Vanity URLs should redirect to a canonical route

โŒ BAD
get "/summer_offers", to: "promotions#index"
โœ… GOOD
get "/summer_offers", to: redirect("/promotions")
๐Ÿ’ No need to track this if you already use
"non-standard rails routes" from above

If user-facing URLs are needed, those should be created in addition to the canonical URLs and should redirect to the existing routes.

Redirecting vanity URLs to canonical routes ensures that users are always directed to the same consistent, official, and preferred URLs on your website.

Also, search engines consider canonical URLs as the authoritative version of a page. By redirecting vanity URLs to canonical routes, you consolidate link equity, preventing the dilution of SEO efforts across multiple URLs. This enhances your website's search engine rankings and visibility.

Rails Views

The Rails view layer allows you to generate HTML with injected Ruby code. Given you can access pretty much everything from everywhere, this can easily become a mess. Here are a few of my guidelines to prevent so.

No instance variables inside partials

โŒ BAD
# app/views/users/show.html.erb
<%= render "avatar" %>

# app/views/users/_avatar.html.erb
<%= image_tag @user.image %>
โœ… GOOD
# app/views/users/show.html.erb
<%= render "avatar", user: @user %>

# app/views/users/_avatar.html.erb
<%= image_tag user.image %>
๐Ÿ’ TRACK IT TO CHERRYPUSH.COM WITH
{
  name: "instance variables inside partials",
  pattern: /@\w*/,
  include: "**/app/views/**/_*.html.erb",
}

A partial is a reusable view template. It allows you to modularize your pages into easy-to-reuse components. When required data is not explicitly passed into a partial, it makes it more difficult to reuse it later.

If it seems awkward to explicitly pass in each dependent variable, consider moving the behavior elsewhere. For example, into a model.

No instance variables inside helpers

โŒ BAD
def welcome_message
  "Hello #{@user.name}"
end
โœ… GOOD
def welcome_message(user)
  "Hello #{user.name}"
end
๐Ÿ’ TRACK IT TO CHERRYPUSH.COM WITH
{
  name: "instance variables inside partials",
  include: "**/app/**/helpers/**/*.rb",
  pattern: /@\w*/,
}

Same as above, relying on instance variables make it difficult to reuse helper methods. We should always provide them via an explicit parameter.

If it seems awkward to explicitly pass in each dependent variable, consider moving the behavior elsewhere, for example, into a model.

Rails Controllers

Stick to standard controller actions

โŒ BAD
class UserController < ApplicationController
  def notify
    @user.notify!
  end
end
โœ… GOOD
class UserNotificationsController < ApplicationController
  def create
    @user.notify!
  end
end
๐Ÿ’ No need to track this if you already use 
"non-standard rails routes" from above

This goes hand in hand with the above-mentioned rule about non-standard routes, so you don't need to set up a duplicate metric for that. Just make sure you properly set up the previously mentioned metric about routes.

Controller callbacks should be explicit about their scope

โŒ BAD
before_action :set_user
โœ… GOOD
before_action :set_user, only: [:show, :update]
๐Ÿ’ TRACK IT TO CHERRYPUSH.COM WITH
{
  name: "before action without only or except",
  pattern: /\bbefore_action :\w*$/,
  include: CONTROLLERS,
},

Callbacks in Rails controllers should always specify only or except to explicitly define the subset of actions they apply to; this practice enhances code transparency and predictability by clearly indicating the scope of callback execution, preventing inadvertent side effects on unrelated actions, simplifying logic, and contributing to a more organized, maintainable, and secure codebase.

Rails Tests

Prefer let! over let

โŒ BAD
let(:user) { create :user }
โœ… GOOD
let!(:user) { create :user }
๐Ÿ’ TRACK IT TO CHERRYPUSH.COM WITH
{ 
  name: "let without bang!", 
  pattern: /\blet(\(| ):/,
  include: "**/*_test.rb", 
},

Using let! over let in Rails tests is preferable when you need to ensure that the variable is set up immediately, rather than lazily.

Unlike let, which is evaluated only when it's first called within a test, let! guarantees that the setup occurs before each test example that uses it. This is particularly useful when the variable's setup involves creating records in the database or performing other actions that are necessary for the test's context.

By using let!, you ensure that the setup is consistent and available right from the beginning of your test scenarios, reducing the chances of unexpected behavior and making your tests more reliable.

Summary ๐Ÿ’

As you can see above, many of the conventions include a way to track it with Cherry. These trackers provide you with three main functionalities:

Monitoring: Cherry plots graphs with the number of occurrences of your technical debt over time. These are especially useful to measure progress and estimate the amount of work left.

Enforcing: Cherry integrates with your CI/CD and can fail your CI builds in case pull requests introduce new occurrences of a certain metric. For that, you can use the cherry diff command and the option --error-if-increase. More details in the docs here: cherrypush.com/docs

Alerting: Within Cherry, you can opt to "watch" your most critical metrics. This way, you'll be notified of new occurrences, both via the app and via email.

Without further ado, let's set up your .cherry.js file with all the above metrics. If we did it all right, your final file should look something like this:

const CLASS_PATTERN = /class\s/;
const MONOLITH_FILES = "app/**/*.{rb,js,jsx,ts,tsx}";
const RUBY_HELPERS = "**/app/**/helpers/**/*.rb";
const RAILS_PARTIALS = "**/app/views/**/_*.html.erb";

module.exports = {
  project_name: "rsv-ink/majestic_monolith", // your GihHub repo
  plugins: ["rubocop"], // optional plugins you can activate
  metrics: [
    {
      name: "instance variables inside helpers",
      pattern: /@\w*/,
      include: RUBY_HELPERS,
    },
    {
      name: "instance variables inside partials",
      include: RAILS_PARTIALS,
      pattern: /@\w*/,
    },
    {
      name: "non-standard rails routes",
      pattern: /^\s.(get|post|put|delete)\b/,
      include: ["**/routes.rb", "**/routes/*.rb"],
    },
  ],
};

With that, you're ready to push your codebase stats with:

cherry push --api-key=YOUR_API_KEY_HERE

Find your API key at cherrypush.com/user/settings

Did you find this article valuable?

Support Flavio Wuensche by becoming a sponsor. Any amount is appreciated!

ย