Qu’est-ce que Ruby on Rails ?
Rails est un framework web full-stack pour Ruby. Il est livré avec tout ce dont une application web basée sur une base de données a besoin : un ORM, un routeur, une couche de vue, des tâches de fond (background jobs), des mailers, du caching, des WebSockets et un environnement de test. Au lieu de choisir et d’assembler ces composants vous-même, vous héritez d’une pile cohérente conçue pour fonctionner ensemble.
Son idée fondamentale est la convention plutôt que la configuration. Le framework suppose une structure logique — Post se trouve dans app/models/post.rb, correspond à une table posts et est servi par PostsController — et vous n’écrivez de la configuration que lorsque vous vous en écartez. L’équipe de Rails appelle cette philosophie omakase, le choix du chef : faites confiance aux valeurs par défaut, et vous irez plus vite.
C’est grâce à ce compromis que Rails propulse des produits allant de Basecamp et GitHub à Shopify et HEY depuis vingt ans, et pourquoi un seul développeur peut encore déployer une application sérieuse en un week-end.
La convention plutôt que la configuration
Chaque projet Rails possède la même structure, vous n’avez donc jamais à vous demander où placer un élément.
app/
├── controllers/ # request handling
├── models/ # domain and persistence
├── views/ # templates
├── jobs/ # background work
├── mailers/ # email
└── javascript/ # Stimulus controllers
config/
├── routes.rb # the URL map
└── database.yml # connection settings
db/
└── migrate/ # schema history
L’avantage est la prévisibilité. Un nouveau développeur ouvre le dépôt et sait déjà que le modèle User se trouve dans app/models/user.rb et que sa table est users. Le nommage n’est pas un débat de style ; c’est une interface sur laquelle le framework s’appuie.
Le cycle de requête MVC
Rails est un framework Model-View-Controller, et chaque requête HTTP suit le même chemin :
- Le Routing associe l’URL à une action du contrôleur.
- Le contrôleur charge ou modifie des données via les modèles.
- Les modèles appliquent les règles métier et communiquent avec la base de données.
- Une vue génère la réponse, généralement sous forme de HTML.
# config/routes.rb
Rails.application.routes.draw do
resources :posts
root "posts#index"
end
Une requête vers GET /posts arrive sur PostsController#index, qui prépare @posts, après quoi app/views/posts/index.html.erb est rendu. Le fait de garder chaque couche focalisée sur son rôle est ce qui permet à une application Rails de rester maintenable à mesure qu’elle grandit.
La ligne de commande est le framework
Vous touchez à peine à Rails via des fichiers de configuration. La majeure partie du travail s’effectue dans le terminal.
rails new shop --database=postgresql
cd shop
bin/rails generate scaffold Post title:string body:text published:boolean
bin/rails generate model Comment post:references body:text
bin/rails generate controller Pages home about
bin/rails db:create db:migrate
bin/rails server
generate écrit pour vous le modèle, la migration, le contrôleur, les routes, les vues et les tests, et la sortie du générateur vous indique les modifications effectuées. Lisez chaque fichier généré avant de le conserver — le moyen le plus rapide d’apprendre Rails est d’étudier ce qu’il écrit, puis de supprimer ce dont vous n’avez pas besoin.
Active Record : la couche modèle
Active Record est l’ORM. Une classe modèle encapsule une table de base de données, et une instance encapsule une ligne.
Migrations
Les migrations sont des modifications de schéma versionnées et réversibles stockées dans db/migrate. C’est la seule méthode supportée pour modifier la base de données.
class CreatePosts < ActiveRecord::Migration[8.0]
def change
create_table :posts do |t|
t.string :title, null: false
t.text :body, null: false
t.boolean :published, default: false, null: false
t.references :author, null: false, foreign_key: { to_table: :users }
t.timestamps
end
add_index :posts, %i[published created_at]
end
end
Exécutez bin/rails db:migrate pour l’appliquer et bin/rails db:rollback pour l’annuler. Comme change est réversible, Rails sait comment inverser les opérations sans que vous ayez à écrire de méthode down.
Associations
Les associations décrivent les relations et génèrent des méthodes pratiques.
class User < ApplicationRecord
has_many :posts, dependent: :destroy
has_many :comments, through: :posts
end
class Post < ApplicationRecord
belongs_to :author, class_name: "User", inverse_of: :posts
has_many :comments, dependent: :destroy
has_one_attached :cover
end
Une fois cela en place, user.posts, post.author et post.comments.create(body: "...") fonctionnent tous, et dependent: :destroy empêche les lignes orphelines de rester dans la base de données.
Validations et scopes
Les validations protègent le domaine au niveau de la couche modèle, par laquelle passe chaque écriture.
class Post < ApplicationRecord
validates :title, presence: true, length: { maximum: 120 }
validates :body, presence: true
validates :slug, uniqueness: true
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
before_validation :generate_slug, on: :create
private
def generate_slug
self.slug ||= title.to_s.parameterize
end
end
Les scopes retournent des relations, ils sont donc composables : Post.published.recent.limit(20) se lit comme une phrase et s’exécute comme une seule requête. Lorsqu’un scope dépasse une ligne ou deux, transformez-le en méthode de classe pour qu’il reste lisible.
Éviter les requêtes N+1
Le problème de performance le plus courant dans Rails est la requête N+1 : une requête pour une liste, puis une requête supplémentaire pour l’association de chaque ligne.
# Two queries total, no matter how many posts.
posts = Post.includes(:author, :comments).recent
posts.each do |post|
puts "#{post.title} by #{post.author.name}"
end
Utilisez includes lorsque vous accéderez à l’association, preload lorsque vous souhaitez une requête séparée, et eager_load lorsque vous filtrez également sur l’association. Ajoutez la gem bullet à votre environnement de développement et elle signalera chaque N+1 avant qu’il n’atteigne la production.
Contrôleurs et routes RESTful
resources déclare les routes CRUD conventionnelles et les mappe aux actions du contrôleur portant des noms correspondants.
# config/routes.rb
Rails.application.routes.draw do
resources :posts do
resources :comments, only: %i[index create destroy]
end
namespace :api do
namespace :v1 do
resources :posts, only: %i[index show create update destroy]
end
end
end
Les contrôleurs restent légers. Ils chargent les données, appellent le domaine et choisissent une réponse. La logique métier appartient aux modèles ou aux objets de service, pas aux actions.
class PostsController < ApplicationController
before_action :set_post, only: %i[show update destroy]
def index
@posts = Post.published.includes(:author).recent
end
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: "Post created."
else
render :new, status: :unprocessable_entity
end
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :body, :published, tag_ids: [])
end
end
Les strong parameters ne sont pas optionnels. params.require(:post).permit(...) est la frontière qui empêche un client de modifier des colonnes que vous n’aviez pas prévu, telles que admin ou role.
Vues, ERB et layouts
Les vues sont des templates ERB : du HTML avec du Ruby intégré. Rails échappe les sorties par défaut, ce qui élimine toute une catégorie de bugs de cross-site scripting.
<%# app/views/posts/index.html.erb %>
<h1>Posts</h1>
<%= link_to "New post", new_post_path %>
<ul>
<% @posts.each do |post| %>
<li>
<%= link_to post.title, post %>
<span><%= pluralize(post.comments.size, "comment") %></span>
</li>
<% end %>
</ul>
Utilisez <%= %> pour afficher un résultat et <% %> pour exécuter du code sans affichage. Un layout dans app/views/layouts/application.html.erb enveloppe chaque page, et les partials — des fichiers commençant par un underscore — vous permettent de réutiliser du markup avec render "post", post: post ou simplement render @posts.
Hotwire : Turbo et Stimulus
Le Rails moderne ajoute de l’interactivité sans avoir besoin d’une application mono-page (SPA). Turbo intercepte les liens et les formulaires pour remplacer des fragments de page via le réseau ; Stimulus ajoute de petits comportements JavaScript là où vous en avez réellement besoin.
<%# Turbo Frames scope an update to one part of the page %>
<%= turbo_frame_tag "comments" do %>
<%= render @post.comments %>
<% end %>
// app/javascript/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["menu"];
toggle() {
this.menuTarget.classList.toggle("hidden");
}
}
C’est l’amélioration progressive (progressive enhancement) en pratique : le HTML rendu côté serveur est la base, et JavaScript vient l’enrichir. La plupart des applications Rails n’ont jamais besoin d’un routeur côté client.
Assets et import maps
Rails 7 a remplacé l’ancien pipeline Sprockets par les import maps par défaut et propose Propshaft comme pipeline d’assets plus simple. Vous pouvez ainsi “pinner” une bibliothèque sans passer par une étape de build Node.
bin/importmap pin local-time
bin/rails assets:precompile
Si vous préférez un bundler, bin/rails javascript:install:esbuild ou :vite permet de modifier la configuration. Le principe reste le même : servir ce que le navigateur comprend et garder la toolchain en retrait jusqu’à ce qu’elle devienne nécessaire.
Tâches de fond (Background jobs)
Tout ce qui est lent — l’envoi d’un e-mail, l’appel à une API, la génération d’un rapport — doit être placé dans un job, et non dans la requête. Active Job est l’interface commune ; l’adaptateur détermine où les jobs sont exécutés.
class PublishPostJob < ApplicationJob
queue_as :default
retry_on Net::ReadTimeout, wait: :polynomially_longer, attempts: 5
def perform(post_id)
Post.find(post_id).publish!
end
end
PublishPostJob.perform_later(post.id)
# config/application.rb
config.active_job.queue_adapter = :solid_queue # or :sidekiq
Rails 8 est livré avec Solid Queue, qui stocke les jobs dans la base de données et ne nécessite pas Redis, ce qui réduit le nombre de composants mobiles pour une application sur un serveur unique. Sidekiq reste un excellent choix pour les volumes élevés.
Tests
Rails génère des tests pour chaque élément créé. Minitest est inclus par défaut et s’avère très rapide ; RSpec est l’alternative populaire, offrant une syntaxe plus expressive.
# test/models/post_test.rb
require "test_helper"
class PostTest < ActiveSupport::TestCase
test "requires a title" do
post = Post.new(body: "Hello")
assert_not post.valid?
assert_includes post.errors[:title], "can't be blank"
end
end
# spec/models/post_spec.rb
RSpec.describe Post, type: :model do
it "is invalid without a title" do
expect(Post.new(body: "Hello")).not_to be_valid
end
end
Les tests de contrôleur effectuent de réelles requêtes en cours de processus, ce qui permet de détecter des bugs de routage et de paramètres que les tests unitaires ne voient pas.
# test/controllers/posts_controller_test.rb
class PostsControllerTest < ActionDispatch::IntegrationTest
test "lists published posts" do
Post.create!(title: "Hi", body: "There", published: true)
get posts_url
assert_response :success
end
end
Exécutez l’ensemble des tests avec bin/rails test ou bundle exec rspec. Ajoutez bin/rails test:system pour les tests système pilotés par navigateur lorsque le flux utilisateur est suffisamment critique pour nécessiter un test de bout en bout.
La console Rails
bin/rails console démarre votre application avec tous les modèles et helpers disponibles. C’est le moyen le plus rapide d’explorer vos données, de tester une requête ou de reproduire un bug.
bin/rails console
bin/rails console --sandbox # rolls back all changes on exit
Post.published.recent.limit(5)
Post.where(published: false).update_all(published: true)
Post.includes(:author).find_each { |post| puts post.title }
--sandbox sert de filet de sécurité pendant vos expérimentations : toute écriture est annulée (rollback) lorsque vous quittez la console.
Bonnes pratiques
- Gardez vos contrôleurs légers ; déplacez la logique métier dans les modèles ou des objets de service.
- Utilisez le chargement anticipé (eager loading) des associations avec
includeset surveillez les logs pour détecter les requêtes N+1. - Modifiez la base de données uniquement via des migrations, et commitez-les avec le code.
- Utilisez des paramètres forts (strong parameters) pour chaque écriture et effectuez la validation au niveau de la couche modèle.
- Ajoutez des index de base de données pour les colonnes sur lesquelles vous effectuez des filtres et des tris.
- Déléguez les tâches lourdes à des jobs d’arrière-plan plutôt que de les traiter durant le cycle de requête.
- Écrivez un test pour chaque bug corrigé, puis veillez à maintenir la rapidité de votre suite de tests.
Erreurs courantes
- Rendre une liste sans
includeset déployer un problème de requêtes N+1 en production. - Placer la logique métier dans les contrôleurs jusqu’à ce que les actions fassent des centaines de lignes.
- Utiliser
params[:post]directement au lieu des strong parameters. - Modifier
db/schema.rbà la main au lieu d’écrire une migration. - Oublier les options
dependent:et laisser des enregistrements orphelins. - Recourir à un service object ou à une gem avant que le modèle ne le justifie.
- Exécuter des requêtes lourdes dans les vues au lieu de charger les données dans le contrôleur.
Et après ?
Rails enseigne une manière de penser durable : les ressources, les conventions et un emplacement unique pour chaque responsabilité. Si vous appréciez le modèle « batteries-included », Laravel applique la même philosophie en PHP, tandis que Django le fait en Python. Le routage de Rails étant basé sur les ressources, le guide sur les REST APIs explique les concepts sous-jacents. Et puisque la plupart des applications Rails s’appuient sur une base de données relationnelle, approfondissez ensuite vos connaissances en PostgreSQL.