An Introduction to Ruby on Rails – From someone with a frontend background

New to Rails? Let's skim over the basics of the framework!

If you’ve come here looking for a post from a Ruby on Rails expert, I’m sorry to inform you that I’m not that person. What you can expect from this post is an account from a novice, working on a real Rails project for the first time.

Throughout my career, I’ve worked primarily as a frontend developer, particularly using React and TypeScript. I was accustomed to technologies like Vite, Next, and Nest, but Ruby and Rails was well beyond my scope.

Last month, I finished a project here at Codeminer and was assigned to a new one, this time as a full-stack developer on a Rails codebase.

When I first opened the code, I thought something was off. It seemed that several folders and files lacked a clear connection to one another. I knew little about the purpose of most of them and felt a bit lost, so I tried to find any material that could help me understand the framework.

Fortunately, I came across some good references that I will link to at the end of this post. As I was reading and doing my first tasks on this new project, I noticed two things that stood out a lot:

  1. Don’t fight the framework. You’ll want to, I swear; especially if you’ve worked with other technologies before. "Why do I have to do X like this if I’ve always done it like Y?". I understand you, but regardless of whether you’re a Rails novice or a novice in general, going against the framework will give you a hard time.

  2. Rails is a very opinionated framework, and as you work with it, this becomes very evident.

As we’ll see later, there’s a reason for it to be this way. We’ll hover over (and it is indeed a hover) the main points of Rails, its history, why it exists, and what problem it solves. Ready? Let’s hop into it.

Disclaimer: This post was structured based on the latest version of the Rails documentation, which is Rails v8. Depending on when you read this, some details may have changed.

A Little Bit of Definition

Ruby on Rails (RoR) is a framework for the Ruby language that emerged in 2003, being notoriously known for "doing a lot on its own." What do I mean by that? I mean that there seems to be a "traditional" way of using it: a series of conventions that you must respect to operate the framework well. It’s important to understand that these conventions won’t necessarily seem obvious, especially for those of us who are just starting.

From now on, we will focus our introduction on this "traditional way" of using Rails, which consists of working with it in a full-stack manner, that is, covering both the frontend and the backend.

For the sake of curiosity, the "non-traditional way" of using Rails would be to use it only as a Backend, leaving the Frontend decoupled, which requires changing some of its default settings.

That said, a traditional Rails project, in its default settings, basically has the following:

  • a relational database engine (SQLite)
  • an ORM called Active Record
  • a file to create routes
  • a testing environment
  • a template language (ERB)
  • a MVC architecture

Note: If you are following Rails 8 docs, you may notice that more things are included by default, such as a linter (Rubocop) and a vulnerability scanner (Brakeman). Above, I mentioned what has been essentially provided since the first versions of the framework.

In what is considered the Frontend of our application, Rails is based entirely on SSR (Server-Side Rendering), serving HTML files that compose our Views.

In what is considered the Backend, we have Models managed by Active Record, which will map our database entities to their respective code entities.

Finally, we have the Controllers, which can be roughly understood as a bridge so that our Models can interact with our Views and vice versa.

Curiosity: Active Record is the name of a design pattern as well as the name of the Rails ORM. That’s precisely why the Active Record ORM has this name: it’s an implementation of the Active Record design pattern.

A Little Bit of History

I was a child in 2003 and was already used to browsing the Web without adult supervision; those were different times.

Back in the day, the modern concept of a smartphone did not yet exist. Flash was still owned by Macromedia and was practically indispensable if you wanted to play a browser game. A large part of the websites were said to be static, with somewhat limited interactivity, but they had their charm.

If you want to remember, or perhaps see for the first time, what it was like back then, take a look at this link.

Regarding web development, the most popular languages were PHP, Java, and obviously HTML, CSS, and JS. If you were a programmer at that time, you would probably hear about stacks like LAMP and J2EE being used in several real projects, and it was in this context that Rails emerged.

Aiming for practicality and productivity, Ruby on Rails gained popularity by solving some of the frustrations of developers at the time. And what were these frustrations? I can’t say for sure either. I wasn’t a developer, but I did some research, and one thing that stood out was boredom.

Web development in the early 21st century involved a lot of setup and repetitiveness. Depending on the stack, your tasks could involve: lines and lines of boilerplate code, writing SQL queries directly in the code and manually iterating over their results, mixing HTML code with the application’s data and business logic, choosing a way to standardize the project and adapt it to a reasonable folder structure, among other challenges.

With Rails, developers gained new weapons to create better software, and Ruby was the language chosen to support this new ecosystem. But why Ruby?

In the words of Rails creator David Heinemeier Hansson (DHH), taken from the documentary Ruby on Rails: The Documentary, Ruby was chosen because it was an interesting language, which seemed more like pseudo-code and could allow a combination of advantages that were found in PHP and Java ecosystems.

But here’s an important point: Rails was not a cause; it was a consequence. Rails was extracted from another project that the creator was working on at the time, called Basecamp. In other words, the purpose was not to create a framework per se, but the framework emerged based on what was being decided and implemented in the Basecamp project.

A Little Bit of Philosophy

Based on what we saw earlier, I think it’s clearer now that the set of choices made while creating Basecamp, and later Rails, came from ideas and opinions prevalent at the time. With the launch of the framework, these same ideas and opinions became the basis of the so-called Rails Philosophy.

In addition to the elegance and robustness brought by Ruby, Rails is based on two core principles:

  1. CoC (Convention over Configuration): As the name implies, the conventions adopted by Rails will replace part of the manual configuration that the developer would have to do. This point is a clear reflection of what we saw about the boredom faced by web developers back in the day. Why waste your time configuring if you can write code?

    Obviously, this doesn’t mean that you will never need to configure anything in Rails or that it is unconfigurable. In fact, there are several gems (equivalent to libraries if you come from Python or JavaScript) and ways to customize them, but the heavy lifting is done by the framework itself.

  2. DRY (Don’t Repeat Yourself): This principle was incorporated into the Rails Philosophy under the influence of one of the authors of The Pragmatic Programmer, Dave Thomas, whom DHH followed at the time. What is important here is that when developing with Rails, we should strive to write pieces of code that have a clear and unambiguous representation within our system. If you want to go deeper into this principle, refer to The Pragmatic Programmer, 20th anniversary edition, which discusses it a lot.

Again, as we use Rails in a real project, these things become clearer. The fact that a lot is done for us under the hood still causes me a lot of confusion, but I will provide an example of one of these conventions in the next section.

A Little Bit About MVC

Rails is entirely designed on top of an MVC (Model-View-Controller) architecture. Let’s talk about each of the three letters separately, starting with the letter M, and then we’ll see how they interact.

The letter M stands for Models, which are responsible for the business rules and the data structure of our application. Through the Active Record ORM, the Models will serve as a basis for creating our entities in the database, so that a default relationship between a given Model in the code and its corresponding entity in the database exists.

Since Rails uses a relational database by default, our entities will generally be represented as tables, which will find corresponding Ruby classes (Models) in the code. Following this same reasoning, the rows of the tables can be understood as the instances of the classes (objects), and the columns as the attributes that these instances will have.

The V stands for Views, which represent the presentation layer of our application, what the end user generally sees and interacts with. The Views can display data and receive external inputs that will travel through the Controller and the Model.

Finally, we have the C, which represents the Controllers. A View and Model do not interact directly, so it is up to the Controller to receive the data from the View and pass it on to the corresponding Model for processing. It is also the job of the Controller to take this same data, now processed by the Model, and deliver it back to the View.

In this way, the structures and business rules that govern the data we have in our application are decoupled from the presentation logic of that same data! In other words, a piece of data can be presented to the user in different ways, even if, in essence, this same data follows a series of well-established rules and structures.

Precisely because it separates things so well, the MVC architecture is very common and is not particularly exclusive to Rails.

As an analogy, the MVC architecture would be like a restaurant. The restaurant’s customers consult and interact with the menu (View) to choose what they will eat. A waiter (Controller) collects the customers’ orders and takes them to the kitchen.

The restaurant’s chef (Model) receives the information from the waiter and processes it. The ingredients (data) are combined in a recipe (business rules) to produce the meal; a particular way of combining the ingredients according to the recipe. This dish is passed to the waiter again, who now takes it to the customers.

Did you notice that the customers and the chef don’t talk to each other? Have the words written on the menu been converted into a dish? Aren’t the words on the menu and the final dish two different ways of representing a combination of ingredients (data) that follows a recipe (business rules)? This is the magic of MVC.

A Little Bit of an Example

Now that we’ve seen more of MVC, let’s see a practical example of Rails and its conventions. If you’ve ever used a language like Python or JavaScript, you’re probably used to imports and exports of functions and libraries. When you open a Rails project, however, you might be surprised to find that there are no explicit imports and exports almost anywhere.

But how does Rails relate one file to another? How does it know to which Model my Controller should send a set of data and which View should be shown to a user accessing a certain route?

The answer lies in the naming convention and folder structure. Rails doesn’t need explicit configuration because it expects your files and classes to follow a predefined pattern.

Imagine that we are going to work on an e-commerce project and that we will need an entity to represent the products sold. In our terminal, we can run the following command:

# Inside our Rails project folder
$ rails generate scaffold Product name:string description:text price:decimal

This single command instructs Rails to create the entire basic structure for managing products:

  1. A Model to interact with the database.
  2. A Migration to create the corresponding table in the database.
  3. A Controller to manage requests (such as creating, listing, updating, and deleting products).
  4. A complete set of Views for the index, show, new, and edit pages.
  5. The Routes to connect URLs to the controller’s actions.

Let’s briefly skim over each of these points:

First, notice that Rails created a file with the class name in the singular (product.rb) that defines the Product class (singular and capitalized).

Our file is located at app/models/product.rb. We can inspect it to discover that Product is a child class of ApplicationRecord (a standard Active Record class).

# app/models/product.rb
class Product < ApplicationRecord
end

Second, we can see that our Product class will be mapped to a database table called products (plural). This is evident in the migration file, created in db/migrate. We can see that the attributes of our Product class are here, such as name and description.

When we start our database, the migrations will be applied, and our Product model will map to the products table.

Third, let’s look at our Controller located at app/controllers/products_controller.rb. The controller is named in the plural (products_controller.rb) and the class follows the ProductsController pattern (plural and in CamelCase).

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  # GET /products
  def index
    @products = Product.all
  end

  # Other actions like :show, :new, :create etc. can be included here.
end

Actions are methods that exist within controllers and, by default, Rails provides a set of them, each with a specific purpose.

  • index: Displays a list of all resources.
  • show: Displays a specific resource.
  • new: Displays a form for creating a new resource.
  • create: Creates a new resource.
  • edit: Displays a form for editing a resource.
  • update: Updates an existing resource.
  • destroy: Deletes a resource.

Observe the index action of our ProductsController:

  • It uses the Product model (which Rails already knows, without needing an import) to fetch all products.
  • It stores the result in the instance variable @products.
  • Automatically, Rails establishes that the index action will be executed when we make a GET request to the /products route.

By convention, when the index action of the ProductsController finishes, Rails looks for a file called index.html.erb inside a folder with the same name as the controller (app/views/products/). This is our fourth point listed above, we now have a corresponding view for the index action.

The @products variable that we also defined in the index of the ProductsController is "magically" available in the View, that is, we can access it to show the user a list of our products!

<h1>Products</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Description</th>
      <th>Price</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @products.each do |product| %>
      <tr>
        <td><%= product.name %></td>
        <td><%= product.description %></td>
        <td><%= product.price %></td>
        <td><%= link_to 'Show', product %></td>
      </tr>
    <% end %>
  </tbody>
</table>

This way, our data can be presented to the user without much effort!

Finally, in the fifth and last point, let’s look at the config/routes.rb file. Remember that we’ve said that the index action responds to requests sent to /products? Well, this mapping is done in the routes.rb file.

Rails.application.routes.draw do
  resources :products
  # resources automatically creates seven different routes under /products for every single default action listed above (index, new, show, create, delete, update, edit).
end

To consult the created routes, execute the following command in the terminal:

# Inside our Rails project folder
$ rails routes

There are numerous ways to use the Rails router, and the documentation explains it in detail. If you have any questions, consult the Routing section.

In summary, with one command in the terminal, we created a chain that completes itself without any additional configuration lines:

  • Every GET request to the /products URL is directed to the index action of the ProductsController.
  • The controller knows that it should use the Product model.
  • The index.html.erb view in the app/views/products/ folder is rendered automatically.

A Little Bit of Frontend: SSR, ERB, Data Injection, Partials, and Hotwire

To conclude, let’s talk about Frontend. Using Rails the traditional way, we don’t have CSR (Client-Side Rendering), only SSR (Server-Side Rendering). Rails returns our Views files to be rendered in the browser.

By default, these are written in a "template language" called ERB (Embedded Ruby). ERB allows you to write HTML and inject Ruby code directly into it. This way, although the HTML arrives ready from the server for the user, the page itself can benefit from a certain dynamism, since its content can contain blocks of code that run on the server just before the page is delivered.

Curiosity: ERB has a syntax almost identical to EJS (Embedded JavaScript), which plays the same role in full-stack JavaScript applications entirely based on SSR.

In addition to ERB, Views can also benefit from data injection. Remember what we saw in the previous example of creating an instance variable @products that was available in index.html.erb? Data injection is the name given to this automatic injection of the @products variable into the View of our index action.

Cool, but for those who are used to React like me, a question might arise. If I am going to structure my pages as Views that are linked, under the hood, to a Controller and a Model, how can I reuse pieces of HTML? In other words, how to do something similar to a reusable component?

For this, we can create partials, reusable parts of ERB code that can be rendered in the Views. By convention, partials are contained in files whose name begins with _ (underscore) and are rendered using a standard Rails method called render.

For instance, if I have a partial at the path app/views/products/_menu.html.erb, I can render it in another View of /products using the tag “ on it.

Hotwire

So far, we’ve seen that Rails delivers the complete HTML page and that it can even benefit from Ruby code injection. But what if I have an application that requires a lot of user interaction? How can I interact with the DOM or react to certain user interactions?

That’s where Hotwire comes in. Hotwire is an umbrella term that usually indicates the combination of three technologies (Turbo, Stimulus, and Native). Combined, they provide a kind of hybrid experience: we continue to deliver HTML via SSR in the traditional way, but we optimize this delivery with techniques capable of improving the performance of our pages, allowing a little JavaScript to help us.

Let’s talk particularly about Turbo and Stimulus since Native is focused on mobile development, which is not our topic here.

Turbo

Turbo is able to recognize if and where certain events emitted by the user, such as a form submit or a link click, will affect the HTML. This way, Rails fetches a new View and changes the current one based on user interactions.

If the user’s current View resembles the new one, Turbo only changes the parts that are different between them, giving the user a fluid and responsive interaction experience.

According to the Turbo website itself, its purpose is to significantly reduce the amount of JavaScript that needs to be written through three techniques:

  • Turbo Drive: Accelerates links and form submissions, eliminating the need to reload pages.
  • Turbo Frames: Decomposes the page into independent contexts, allowing for lazy loading.
  • Turbo Streams: Deliver page changes via WebSocket, SSE (Server-Sent Event), or in response to form submissions.

Stimulus

Although Turbo reduces the amount of JavaScript we need to write, we might need it anyway – sorry, JS haters. Interacting with dropdowns, showing or hiding modals, collapsing some elements on click, etc., are common Web page events.

Stimulus allows us to add a complex layer of interactions to our Views, creating controllers that are attached to and invoked by the very tags that make up our HTML.

We can associate a controller with an HTML tag through an attribute called data-controller. Doing so, we can invoke JavaScript code through our Views.

Let’s look at the following example:

<div data-controller="hello">
  <input data-hello-target="name" type="text" />

  <button data-action="click->hello#greet">Greet</button>

  <span data-hello-target="output"> </span>
</div>
// hello_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["name", "output"];

  greet() {
    this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!`;
  }
}

Note that the Greet button has a click data-action associated with it. When this button is clicked, this data-action is triggered and the greet function of the hello controller is invoked.

Rails finds the right controller by looking for the hello_controller.js file and invokes the greet function in it. In turn, the greet function recognizes its targets, HTML elements that we have marked as important to observe.

Based on the names of the targets, it is possible to extract data about them. See above how we get the value attribute of the input tag , which was labeled as nameTarget by data-hello-target="name". With value in hand, we can pass it to the span labeled as outputTarget by data-hello-target="output".

This is the main example of the Stimulus website. Wanna check the final result? Just follow this link!

Important: Yes, Stimulus also uses the term controllers, but the definition of controllers in Stimulus is not the same as in MVC. A controller in MVC is the intermediary between a Model and a View, whereas a controller in Stimulus is just a JavaScript class that inherits from the Controller class of the @hotwired/stimulus package.

Final Words

I hope this post has helped you, even if a little, to understand Rails better. I wrote it as a way to consolidate what I have understood so far about the framework, so feedback is welcome.

As stated, below are some references that I found and that helped a lot. Thanks for reading, see you next time!

Bonus: Active vs Action

You might have noticed that several parts of Rails have Active or Action in their names, such as Active Record, ActionMailer, ActiveModel, ActionView, etc.

But what does this mean? It’s another convention. Generally, things that start with Action will perform some action (surprised Pikachu) in your Rails project or interact with the external environment, like sending an email.

Things that start with Active are more like utilities to facilitate development, such as ActiveRecord, which plays the role of an ORM, or ActiveAdmin to manage our application with admin privileges.

References

Getting Started – Rails
Stimulus
Rails has two default stacks
Hotrails
Ruby on Rails: The Documentary
RailsConf 2017: In Relentless Pursuit of REST by Derek Prior
Web Design Museum

We want to work with you. Check out our Services page!

Luiz Felipe Diniz

Full stack developer

View all posts by Luiz Felipe Diniz →