All Guides
Intermédiaire-Avancé • 200 h200 h estimées (cours, ateliers, projets)Free Guide

Web Development & AI Software

200 h pour devenir développeur full-stack : Ruby, Rails, JavaScript, SQL, puis intégrez l'IA (OpenAI, Claude Code, agents) dans vos applications web déployées.

Section 13.1.1 : Terminal et ligne de commande

🎯 Objectif pédagogique

Maîtriser le terminal comme outil principal de développement. À la fin de cette section, vous naviguerez dans votre système de fichiers, manipulerez des fichiers et exécuterez des commandes complexes avec confiance.


Le terminal : votre cockpit de développeur

Tout développeur professionnel passe une partie significative de son temps dans le terminal. C'est l'interface textuelle qui vous donne un contrôle total sur votre machine, vos fichiers, vos programmes et vos serveurs. Si l'interface graphique est le volant d'une voiture, le terminal est le cockpit d'un avion : plus complexe, mais infiniment plus puissant.

Le terminal (aussi appelé shell, console ou CLI — Command Line Interface) est un interpréteur qui exécute des commandes textuelles. Sur macOS et Linux, vous utiliserez principalement Bash ou Zsh. Sur Windows, PowerShell ou le Windows Terminal avec WSL (Windows Subsystem for Linux).

Anatomie d'une commande

Une commande suit toujours la même structure :

commande [options] [arguments]

# Exemples :
ls -la Documents/
#  │  │    └── argument (le dossier cible)
#  │  └── options (-l pour détaillé, -a pour fichiers cachés)
#  └── commande (lister les fichiers)

Le système de fichiers est organisé en arborescence. Votre position actuelle est le répertoire courant (working directory). Toute commande s'exécute relativement à cette position.

# Où suis-je ?
pwd                          # Print Working Directory

# Que contient ce dossier ?
ls                           # Liste simple
ls -l                        # Détaillé (permissions, taille, date)
ls -la                       # Avec fichiers cachés (.gitignore, .env)
ls -lah                      # Avec tailles lisibles (Ko, Mo, Go)

# Se déplacer
cd Documents                 # Entrer dans Documents
cd ..                        # Remonter d'un niveau
cd ../..                     # Remonter de deux niveaux
cd ~                         # Aller au répertoire home
cd -                         # Retourner au dernier répertoire

Astuce productivité

Utilisez la touche Tab pour l'autocomplétion. Tapez les premières lettres d'un nom de fichier ou dossier, puis appuyez sur Tab. Le terminal complète automatiquement. En cas d'ambiguïté, appuyez deux fois sur Tab pour voir les options.

Manipulation de fichiers et dossiers

# Créer
touch index.html             # Créer un fichier vide
mkdir mon-projet             # Créer un dossier
mkdir -p src/components/ui   # Créer une arborescence complète

# Copier
cp fichier.txt copie.txt     # Copier un fichier
cp -r dossier/ backup/       # Copier un dossier récursivement

# Déplacer / Renommer
mv ancien.txt nouveau.txt    # Renommer
mv fichier.txt Documents/    # Déplacer

# Supprimer (ATTENTION : pas de corbeille !)
rm fichier.txt               # Supprimer un fichier
rm -r dossier/               # Supprimer un dossier
rm -ri dossier/              # Supprimer avec confirmation

# Lire le contenu
cat fichier.txt              # Afficher tout le contenu
head -20 fichier.txt         # Les 20 premières lignes
tail -20 fichier.txt         # Les 20 dernières lignes
less fichier.txt             # Navigation pagée (q pour quitter)

Recherche et filtrage

# Chercher du texte dans des fichiers
grep 'def initialize' app/models/*.rb    # Chercher dans les fichiers Ruby
grep -r 'TODO' src/                      # Recherche récursive
grep -rn 'function' --include='*.js'     # Avec numéros de ligne

# Trouver des fichiers
find . -name '*.rb'                      # Tous les fichiers .rb
find . -type d -name 'node_modules'      # Trouver des dossiers

# Compter
wc -l fichier.txt                        # Nombre de lignes
wc -w fichier.txt                        # Nombre de mots

Le pipe : la puissance de la composition

Le pipe (|) connecte la sortie d'une commande à l'entrée de la suivante, permettant de créer des pipelines de traitement de données :

# Compter les erreurs dans un log
cat server.log | grep 'ERROR' | wc -l

# Extraire les emails uniques d'un CSV
cut -d',' -f3 contacts.csv | sort | uniq | grep '@'

# Top 5 des plus gros fichiers
find . -type f -exec ls -la {} \; | sort -k5 -rn | head -5
Loading diagram…

Variables d'environnement

Les variables d'environnement sont des valeurs globales accessibles par tous les programmes :

echo $HOME                   # Votre répertoire home
echo $PATH                   # Dossiers où chercher les commandes
echo $USER                   # Votre nom d'utilisateur

# Définir une variable
export API_KEY='sk-abc123'

# Persistance dans .zshrc
echo 'export API_KEY="sk-abc123"' >> ~/.zshrc
source ~/.zshrc

Exercice pratique : créer un projet web

# 1. Créer la structure
mkdir -p mon-site/css mon-site/js mon-site/images
touch mon-site/index.html mon-site/css/style.css mon-site/js/app.js

# 2. Vérifier
find mon-site -type f

# 3. Archiver
tar -czf mon-site.tar.gz mon-site/
ls -lh mon-site.tar.gz

Section 13.1.2 : Git et GitHub

🎯 Objectif pédagogique

Comprendre et utiliser Git pour le versionnement de code et GitHub pour la collaboration. Vous saurez créer des repositories, committer, brancher, merger et travailler en équipe.


Pourquoi Git est indispensable

Imaginez que vous écrivez un roman. Vous faites des modifications, revenez en arrière, essayez des variantes... Sans système de versionnement, vous finiriez avec des fichiers comme roman_v2_final_VRAIMENT_FINAL_ok.docx. En développement, ce chaos serait catastrophique.

Git est un système de contrôle de version distribué créé en 2005 par Linus Torvalds (le créateur de Linux). Il enregistre chaque modification dans un historique permanent, vous permettant de revenir à n'importe quelle version, travailler en parallèle sur plusieurs fonctionnalités via des branches, et collaborer sans conflits.

Les concepts fondamentaux

Loading diagram…

Git organise votre travail en trois zones : le Working Directory (vos fichiers actuels), la Staging Area (modifications sélectionnées pour le prochain commit), et le Repository (historique complet des commits).

Configuration initiale

git config --global user.name 'Alice Martin'
git config --global user.email 'alice@example.com'
git config --global core.editor 'code --wait'
git config --global color.ui auto
git config --list

Votre premier repository

mkdir mon-projet && cd mon-projet
git init

touch index.html style.css app.js
echo '# Mon Projet' > README.md

git status
git add .
git commit -m 'Initial commit: structure du projet'

Le cycle de travail quotidien

# 1. Modifier des fichiers
echo '<h1>Hello World</h1>' >> index.html

# 2. Voir les modifications
git status                   # Fichiers modifiés
git diff                     # Détail ligne par ligne

# 3. Stager
git add index.html

# 4. Committer
git commit -m 'feat: add heading to homepage'

# 5. Historique
git log --oneline --graph

Conventional Commits

Adoptez cette convention dès le début — feat: nouvelle fonctionnalité, fix: correction, docs: documentation, style: formatage, refactor: restructuration, test: tests. Exemple : feat: add user authentication with Devise

Les branches : travailler en parallèle

Les branches sont la fonctionnalité qui rend Git indispensable en équipe. Une branche est une copie indépendante de votre code pour expérimenter sans casser la version stable.

git checkout -b feature/navbar
# ... coder la fonctionnalité ...
git add .
git commit -m 'feat: add navigation bar'

git checkout main
git merge feature/navbar
git branch -d feature/navbar
Loading diagram…

GitHub : collaborer dans le cloud

GitHub ajoute à Git des fonctionnalités essentielles : hébergement de repos, pull requests, issues, CI/CD, et un profil qui fait office de CV technique.

git remote add origin https://github.com/alice/mon-projet.git
git push -u origin main
git pull origin main
git clone https://github.com/lewagon/fullstack-challenges.git

Pull Requests et code review

Le workflow professionnel repose sur les Pull Requests : créer une branche, coder, pousser, ouvrir une PR, l'équipe review, merger dans main après approbation.

.gitignore

node_modules/          # Dépendances npm
.env                   # Variables secrètes
*.log                  # Logs
.DS_Store              # Fichiers macOS
dist/                  # Fichiers compilés

Section 13.1.3 : Ruby — Variables, types et boucles

🎯 Objectif pédagogique

Découvrir le langage Ruby, comprendre ses types de données fondamentaux et maîtriser les structures de contrôle. Ruby est le langage au cœur de Ruby on Rails — sa philosophie de « developer happiness » en fait un excellent premier langage.


Pourquoi Ruby ?

Ruby est un langage créé en 1995 par Yukihiro « Matz » Matsumoto au Japon. Sa philosophie : « Ruby is designed to make programmers happy ». Le langage privilégie la lisibilité et l'expressivité, ce qui en fait un excellent choix pour apprendre la programmation.

Variables et types de données

En Ruby, pas besoin de déclarer le type — le langage le déduit automatiquement (typage dynamique).

# Strings
prenom = 'Alice'
nom = "Martin"
message = "Bonjour, #{prenom} #{nom} !"  # Interpolation

# Nombres
age = 28                    # Integer
prix = 19.99                # Float
quantite = 1_000_000        # Underscore pour lisibilité

# Booléens
majeur = true
inscrit = false

# Nil
resultat = nil

# Symboles
statut = :actif
role = :admin

Opérations sur les strings

nom = 'alice martin'
nom.upcase        # 'ALICE MARTIN'
nom.capitalize    # 'Alice martin'
nom.length        # 12
nom.include?('alice')  # true
nom.gsub('alice', 'bob')  # 'bob martin'
nom.split(' ')    # ['alice', 'martin']
nom.reverse       # 'nitram ecila'
nom.empty?        # false
nom.start_with?('alice')  # true

Opérations sur les nombres

10 + 3    # 13
10 - 3    # 7
10 * 3    # 30
10 / 3    # 3 (division entière !)
10.0 / 3  # 3.333...
10 % 3    # 1 (modulo)
2 ** 10   # 1024 (puissance)

'42'.to_i    # 42
42.to_s      # '42'
42.to_f      # 42.0
-5.abs       # 5
42.even?     # true

Structures conditionnelles

age = 25

if age >= 18
  puts 'Vous êtes majeur'
elsif age >= 16
  puts 'Presque majeur'
else
  puts 'Vous êtes mineur'
end

# Opérateur ternaire
statut = age >= 18 ? 'majeur' : 'mineur'

# unless (inverse de if)
unless age >= 18
  puts 'Accès refusé'
end

# Condition postfix
puts 'Bienvenue !' if age >= 18

# case / when
role = :admin
case role
when :admin then puts 'Accès total'
when :editor then puts 'Accès écriture'
when :viewer then puts 'Accès lecture'
else puts 'Rôle inconnu'
end

Boucles

# while
compteur = 0
while compteur < 5
  puts "Tour #{compteur}"
  compteur += 1
end

# times (la manière Ruby)
5.times do |i|
  puts "Itération #{i}"
end

# upto / downto
1.upto(5) { |i| puts i }
5.downto(1) { |i| puts i }

# loop avec break
loop do
  puts 'Entrez votre nom :'
  nom = gets.chomp
  break if nom.length >= 2
  puts 'Nom trop court !'
end

Arrays (tableaux)

fruits = ['pomme', 'banane', 'cerise']
nombres = [1, 2, 3, 4, 5]

fruits[0]         # 'pomme'
fruits[-1]        # 'cerise'
fruits << 'mangue'
fruits.push('kiwi')
fruits.delete('banane')

fruits.each { |fruit| puts fruit }

nombres.map { |n| n * 2 }       # [2, 4, 6, 8, 10]
nombres.select { |n| n.even? }   # [2, 4]
nombres.reject { |n| n > 3 }     # [1, 2, 3]
nombres.reduce(:+)               # 15
nombres.min                      # 1
nombres.max                      # 5

Section 13.1.4 : Ruby — Hash, méthodes et blocs

🎯 Objectif pédagogique

Maîtriser les Hash (dictionnaires), écrire des méthodes Ruby propres et comprendre les blocs — le mécanisme le plus élégant et distinctif du langage Ruby.


Hash : les dictionnaires Ruby

Un Hash est une collection de paires clé-valeur. Contrairement à un Array indexé par position, un Hash permet d'accéder aux valeurs par une clé nommée.

# Syntaxe moderne (clés symboles - PRÉFÉRÉ)
personne = { nom: 'Alice', age: 28, ville: 'Paris' }
personne[:nom]    # 'Alice'
personne[:age]    # 28

# Ajouter / Modifier
personne[:email] = 'alice@example.com'
personne[:age] = 29

# Supprimer
personne.delete(:ville)

# Vérifications
personne.key?(:nom)      # true
personne.empty?           # false
personne.length           # 3

Symboles vs Strings comme clés

Utilisez toujours des symboles (:nom) comme clés de Hash. Les symboles sont plus rapides (mêmes objets en mémoire) et plus idiomatiques en Ruby. La syntaxe nom: 'valeur' est du sucre syntaxique pour :nom => 'valeur'.

Itération sur les Hash

personne = { nom: 'Alice', age: 28, email: 'alice@example.com' }

personne.each do |cle, valeur|
  puts "#{cle}: #{valeur}"
end

personne.keys     # [:nom, :age, :email]
personne.values   # ['Alice', 28, 'alice@example.com']

personne.map { |k, v| "#{k}=#{v}" }
personne.select { |k, v| v.is_a?(String) }

Hash imbriqués

utilisateurs = {
  alice: {
    nom: 'Alice Martin',
    age: 28,
    competences: ['Ruby', 'JavaScript', 'SQL'],
    adresse: { ville: 'Paris', cp: '75001' }
  },
  bob: {
    nom: 'Bob Dupont',
    age: 32,
    competences: ['Python', 'Docker', 'AWS'],
    adresse: { ville: 'Lyon', cp: '69001' }
  }
}

utilisateurs[:alice][:nom]                  # 'Alice Martin'
utilisateurs[:alice][:adresse][:ville]      # 'Paris'
utilisateurs.dig(:alice, :adresse, :ville)  # 'Paris' (accès sécurisé)
utilisateurs.dig(:charlie, :nom)            # nil (pas d erreur)

Les méthodes

En Ruby, les méthodes retournent implicitement la dernière expression évaluée — pas besoin de return.

def saluer(prenom, langue = 'fr')
  case langue
  when 'fr' then "Bonjour, #{prenom} !"
  when 'en' then "Hello, #{prenom}!"
  when 'es' then "Hola, #{prenom}!"
  else "Hi, #{prenom}!"
  end
end

saluer('Alice')       # Bonjour, Alice !
saluer('Bob', 'en')   # Hello, Bob!

# Keyword arguments
def creer_utilisateur(nom:, email:, role: :viewer)
  { nom: nom, email: email, role: role }
end

creer_utilisateur(nom: 'Alice', email: 'alice@test.com')

# Méthodes prédicats (?), destructives (!)
def majeur?(age)
  age >= 18
end

Les blocs : la signature de Ruby

Les blocs sont des morceaux de code anonymes passés à une méthode. Deux syntaxes :

# do...end (multi-ligne)
[1, 2, 3].each do |nombre|
  puts nombre * 2
end

# { } (une ligne)
[1, 2, 3].each { |nombre| puts nombre * 2 }

Créer ses propres méthodes avec yield

def avec_timing
  debut = Time.now
  yield
  duree = Time.now - debut
  puts "Exécuté en #{duree.round(3)} secondes"
end

avec_timing do
  sleep(1)
  puts 'Travail en cours...'
end

Procs et Lambdas

carre = Proc.new { |n| n ** 2 }
carre.call(5)           # 25
[1, 2, 3].map(&carre)   # [1, 4, 9]

double = ->(n) { n * 2 }
double.call(5)           # 10
[1, 2, 3].map(&double)   # [2, 4, 6]

Section 13.1.5 : Ruby — Classes et TDD avec Rake

🎯 Objectif pédagogique

Comprendre la programmation orientée objet en Ruby via les classes, et adopter le TDD (Test-Driven Development) avec Rake pour écrire du code fiable dès le départ.


Les classes : modéliser le monde réel

La programmation orientée objet (OOP) modélise votre programme comme un ensemble d'objets qui interagissent. Une classe est le plan de construction, un objet est une instance concrète.

class Utilisateur
  attr_accessor :nom, :email
  attr_reader :id

  def initialize(nom, email)
    @id = rand(1000..9999)
    @nom = nom
    @email = email
    @date_inscription = Time.now
  end

  def presentation
    "#{@nom} (#{@email}) - inscrit le #{@date_inscription.strftime('%d/%m/%Y')}"
  end

  def email_valide?
    @email.include?('@') && @email.include?('.')
  end

  def to_s
    presentation
  end
end

alice = Utilisateur.new('Alice Martin', 'alice@example.com')
puts alice.nom           # Alice Martin
puts alice.email_valide? # true

Variables d'instance vs de classe

class Compteur
  @@total = 0              # Variable de classe (partagée)

  def initialize(nom)
    @nom = nom             # Variable d instance (unique)
    @@total += 1
  end

  def self.total
    @@total
  end
end

Compteur.new('A')
Compteur.new('B')
Compteur.total  # 2

Encapsulation : public, private, protected

class CompteBancaire
  def initialize(titulaire, solde)
    @titulaire = titulaire
    @solde = solde
  end

  def deposer(montant)
    raise ArgumentError, 'Montant invalide' unless montant > 0
    @solde += montant
    historiser("Dépôt de #{montant} EUR")
  end

  def retirer(montant)
    raise ArgumentError, 'Montant invalide' unless montant > 0
    raise 'Solde insuffisant' unless solde_suffisant?(montant)
    @solde -= montant
  end

  def afficher_solde
    "Solde de #{@titulaire}: #{@solde} EUR"
  end

  private

  def solde_suffisant?(montant)
    @solde >= montant
  end

  def historiser(operation)
    puts "[#{Time.now}] #{operation}"
  end
end

TDD : Test-Driven Development

Le TDD suit un cycle en trois étapes : Red (test qui échoue), Green (code minimal), Refactor (améliorer).

Loading diagram…

Minitest en pratique

# test/calculatrice_test.rb
require 'minitest/autorun'
require_relative '../lib/calculatrice'

class CalculatriceTest < Minitest::Test
  def setup
    @calc = Calculatrice.new
  end

  def test_addition
    assert_equal 5, @calc.additionner(2, 3)
  end

  def test_division_par_zero
    assert_raises(ZeroDivisionError) do
      @calc.diviser(10, 0)
    end
  end
end
# lib/calculatrice.rb
class Calculatrice
  def additionner(a, b)
    a + b
  end

  def diviser(a, b)
    raise ZeroDivisionError if b.zero?
    a / b
  end
end

Rake : automatiser les tâches

# Rakefile
require 'rake/testtask'

Rake::TestTask.new do |t|
  t.pattern = 'test/**/*_test.rb'
  t.verbose = true
end

task default: :test
rake               # Lance tous les tests
rake test          # Idem, explicitement

Exercice : classe Restaurant

class Restaurant
  attr_reader :nom, :ville

  def initialize(nom, ville, cuisine)
    @nom = nom
    @ville = ville
    @cuisine = cuisine
    @notes = []
  end

  def noter(note)
    raise ArgumentError unless (1..5).include?(note)
    @notes << note
  end

  def note_moyenne
    return 0 if @notes.empty?
    (@notes.sum.to_f / @notes.length).round(1)
  end

  def populaire?
    note_moyenne >= 4.0 && @notes.length >= 5
  end

  def to_s
    "#{@nom} (#{@ville}) - #{@cuisine} - #{note_moyenne}/5"
  end
end

Section 13.2.1 : Héritage et modules Ruby

🎯 Objectif pédagogique

Comprendre l'héritage de classes et les modules Mixin en Ruby pour structurer du code réutilisable et bien organisé. Ces concepts sont fondamentaux pour comprendre Ruby on Rails.


L'héritage : spécialiser des classes

L'héritage permet de créer une classe enfant qui hérite de toutes les propriétés et méthodes d'une classe parent, tout en ajoutant ses spécificités. En Ruby, chaque classe ne peut hériter que d'une seule classe parent (héritage simple).

class Animal
  attr_reader :nom, :age

  def initialize(nom, age)
    @nom = nom
    @age = age
  end

  def parler
    '...'
  end

  def presenter
    "Je suis #{@nom}, j'ai #{@age} ans"
  end
end

class Chien < Animal
  def parler
    'Woof!'
  end

  def fetch(objet)
    "#{@nom} rapporte #{objet}"
  end
end

class Chat < Animal
  def parler
    'Miaou!'
  end

  def ronronner
    "#{@nom} ronronne..."
  end
end

rex = Chien.new('Rex', 3)
puts rex.presenter    # Hérité du parent
puts rex.parler       # Surchargé
puts rex.fetch('balle')  # Spécifique au Chien

Le mot-clé super

super appelle la méthode du même nom dans la classe parent :

class Vehicule
  attr_reader :marque, :modele

  def initialize(marque, modele)
    @marque = marque
    @modele = modele
    @kilometrage = 0
  end

  def info
    "#{@marque} #{@modele} - #{@kilometrage} km"
  end
end

class VehiculeElectrique < Vehicule
  def initialize(marque, modele, autonomie)
    super(marque, modele)
    @autonomie = autonomie
    @batterie = 100
  end

  def info
    "#{super} | Batterie: #{@batterie}% | Autonomie: #{@autonomie} km"
  end
end

tesla = VehiculeElectrique.new('Tesla', 'Model 3', 560)
puts tesla.info

Les modules : Mixins pour la réutilisation

Ruby n'a pas d'héritage multiple, mais les modules (Mixins) permettent de partager du comportement entre classes non liées.

module Affichable
  def afficher
    puts to_s
  end
end

module Validable
  def valide?
    erreurs.empty?
  end

  def erreurs
    []
  end
end

class Produit
  include Affichable
  include Validable

  attr_accessor :nom, :prix

  def initialize(nom, prix)
    @nom = nom
    @prix = prix
  end

  def to_s
    "#{@nom} - #{@prix} EUR"
  end

  def erreurs
    errs = []
    errs << 'Nom requis' if @nom.nil? || @nom.empty?
    errs << 'Prix positif requis' if @prix.nil? || @prix <= 0
    errs
  end
end

p = Produit.new('MacBook Pro', 2399)
p.afficher    # De Affichable
p.valide?     # De Validable

Comparable et Enumerable

class Etudiant
  include Comparable

  attr_accessor :nom, :moyenne

  def initialize(nom, moyenne)
    @nom = nom
    @moyenne = moyenne
  end

  def <=>(other)
    @moyenne <=> other.moyenne
  end
end

etudiants = [
  Etudiant.new('Alice', 16.5),
  Etudiant.new('Bob', 14.2),
  Etudiant.new('Charlie', 18.0)
]

etudiants.sort.map(&:nom)  # ['Bob', 'Alice', 'Charlie']
etudiants.max.nom           # 'Charlie'
Loading diagram…

Section 13.2.2 : Design patterns fondamentaux

🎯 Objectif pédagogique

Découvrir les design patterns les plus courants et comprendre quand les appliquer. Ces patterns sont des solutions éprouvées à des problèmes récurrents en conception logicielle.


Qu'est-ce qu'un design pattern ?

Un design pattern est une solution réutilisable à un problème courant. Ce sont des schémas de pensée qui guident l'organisation du code, décrits dans le livre « Design Patterns » de la Gang of Four (GoF) en 1994.

Le pattern Iterator

Ruby l'intègre nativement grâce aux blocs et Enumerable :

class Playlist
  include Enumerable

  def initialize
    @chansons = []
  end

  def ajouter(chanson)
    @chansons << chanson
  end

  def each(&block)
    @chansons.each(&block)
  end
end

playlist = Playlist.new
playlist.ajouter('Bohemian Rhapsody')
playlist.ajouter('Imagine')
playlist.map(&:upcase)
playlist.count

Le pattern Strategy

Changer le comportement via un lambda ou bloc :

class Trieur
  def initialize(strategie = nil)
    @strategie = strategie || ->(a, b) { a <=> b }
  end

  def trier(collection)
    collection.sort(&@strategie)
  end
end

produits = [
  { nom: 'MacBook', prix: 2399 },
  { nom: 'iPad', prix: 899 },
  { nom: 'iPhone', prix: 1199 }
]

par_prix = ->(a, b) { a[:prix] <=> b[:prix] }
par_nom = ->(a, b) { a[:nom] <=> b[:nom] }

Trieur.new(par_prix).trier(produits)
Trieur.new(par_nom).trier(produits)

Le pattern Observer

Notifier des abonnés quand un événement se produit — Rails l'utilise avec les callbacks :

module Observable
  def self.included(base)
    base.instance_variable_set(:@observers, [])
    base.extend(ClassMethods)
  end

  module ClassMethods
    def add_observer(observer)
      @observers << observer
    end

    def observers
      @observers
    end
  end

  def notify_observers(event, data = {})
    self.class.observers.each do |obs|
      obs.update(event, data) if obs.respond_to?(:update)
    end
  end
end

Le pattern Template Method

Définir le squelette d'un algorithme dans le parent, laisser les sous-classes redéfinir certaines étapes :

class DataExporter
  def export(data)
    header = generate_header
    body = generate_body(data)
    footer = generate_footer
    "#{header}\n#{body}\n#{footer}"
  end

  private

  def generate_header
    raise NotImplementedError
  end

  def generate_body(data)
    raise NotImplementedError
  end

  def generate_footer
    '--- Fin ---'
  end
end

class CSVExporter < DataExporter
  def generate_header
    'nom,email,age'
  end

  def generate_body(data)
    data.map { |row| row.values.join(',') }.join("\n")
  end
end
Loading diagram…

Section 13.2.3 : Architecture MVC

🎯 Objectif pédagogique

Comprendre le pattern MVC qui structure toute application Rails, et l'implémenter en Ruby pur avant de passer au framework.


MVC : séparer les responsabilités

  • Model : données et logique métier
  • View : présentation et affichage
  • Controller : orchestration entre Model et View
Loading diagram…

MVC en Ruby pur

# Model
class Recette
  attr_accessor :nom, :description
  attr_reader :id

  @@toutes = []
  @@prochain_id = 1

  def initialize(attrs = {})
    @id = @@prochain_id
    @@prochain_id += 1
    @nom = attrs[:nom]
    @description = attrs[:description]
    @@toutes << self
  end

  def self.all
    @@toutes
  end

  def self.find(id)
    @@toutes.find { |r| r.id == id }
  end

  def to_s
    "#{@nom} - #{@description}"
  end
end

# View
class RecetteView
  def afficher_liste(recettes)
    puts "\n--- Mes Recettes ---"
    recettes.each_with_index do |r, i|
      puts "#{i + 1}. #{r}"
    end
  end

  def demander_nom
    print 'Nom : '
    gets.chomp
  end
end

# Controller
class RecetteController
  def initialize
    @view = RecetteView.new
  end

  def liste
    @view.afficher_liste(Recette.all)
  end

  def creer
    nom = @view.demander_nom
    Recette.new(nom: nom)
  end
end

# Router
class Router
  def initialize(controller)
    @controller = controller
  end

  def run
    loop do
      puts "\n1. Lister  2. Ajouter  3. Quitter"
      choix = gets.chomp.to_i
      case choix
      when 1 then @controller.liste
      when 2 then @controller.creer
      when 3 then break
      end
    end
  end
end

Section 13.2.4 : Parsing et consommation d'API

🎯 Objectif pédagogique

Lire des fichiers structurés (CSV, JSON) et consommer des APIs REST en Ruby — compétences essentielles pour des applications connectées.


Parsing CSV

require 'csv'

CSV.foreach('clients.csv', headers: true) do |row|
  puts "#{row['nom']} - #{row['email']}"
end

# Écrire un CSV
CSV.open('export.csv', 'w') do |csv|
  csv << ['nom', 'email', 'age']
  csv << ['Alice', 'alice@test.com', 28]
  csv << ['Bob', 'bob@test.com', 32]
end

Parsing JSON

require 'json'

json_string = '{"nom": "Alice", "age": 28}'
data = JSON.parse(json_string)
data['nom']

# Avec clés symboliques
data = JSON.parse(json_string, symbolize_names: true)
data[:nom]

# Générer du JSON
utilisateur = { nom: 'Alice', age: 28 }
JSON.generate(utilisateur)
JSON.pretty_generate(utilisateur)

Consommer une API REST

require 'open-uri'
require 'json'

url = 'https://api.github.com/users/lewagon'
response = URI.open(url).read
user = JSON.parse(response)
puts user['name']
puts user['public_repos']

La gem rest-client

require 'rest-client'
require 'json'

# GET
response = RestClient.get('https://api.example.com/users')
users = JSON.parse(response.body)

# POST
response = RestClient.post(
  'https://api.example.com/users',
  { nom: 'Alice', email: 'alice@test.com' }.to_json,
  { content_type: :json, accept: :json }
)

# Gestion d erreurs
begin
  RestClient.get('https://api.example.com/unknown')
rescue RestClient::NotFound
  puts '404 - Non trouvé'
rescue RestClient::ExceptionWithResponse => e
  puts "Erreur: #{e.response.code}"
end

Exercice : CLI météo

require 'open-uri'
require 'json'

class MeteoService
  BASE_URL = 'https://api.open-meteo.com/v1/forecast'

  def temperature(lat, lon)
    url = "#{BASE_URL}?latitude=#{lat}&longitude=#{lon}&current_weather=true"
    data = JSON.parse(URI.open(url).read)
    data['current_weather']['temperature']
  end
end

service = MeteoService.new
temp = service.temperature(48.8566, 2.3522)
puts "Paris : #{temp} C"

Section 13.2.5 : Livecodes et challenges

🎯 Objectif pédagogique

S'entraîner à résoudre des problèmes algorithmiques en Ruby dans un format livecode. Ces exercices développent la pensée logique et préparent aux entretiens techniques.


La méthodologie UPER

  1. Understand : reformulez le problème, identifiez inputs/outputs/cas limites
  2. Plan : pseudo-code en français
  3. Execute : traduire en Ruby
  4. Review : tester et optimiser

Challenge : Palindrome

def palindrome?(mot)
  cleaned = mot.downcase.gsub(/[^a-z0-9]/, '')
  cleaned == cleaned.reverse
end

puts palindrome?('kayak')       # true
puts palindrome?('hello')       # false

Challenge : Anagrammes

def anagrammes?(mot1, mot2)
  normaliser = ->(m) { m.downcase.delete(' ').chars.sort }
  normaliser.call(mot1) == normaliser.call(mot2)
end

puts anagrammes?('listen', 'silent')  # true

Challenge : Chiffre de César

def caesar_cipher(text, shift)
  text.chars.map do |char|
    if char.match?(/[a-z]/)
      ((char.ord - 97 + shift) % 26 + 97).chr
    elsif char.match?(/[A-Z]/)
      ((char.ord - 65 + shift) % 26 + 65).chr
    else
      char
    end
  end.join
end

puts caesar_cipher('Hello World', 3)  # Khoor Zruog

Challenge : Mot le plus fréquent

def mot_frequent(texte)
  mots = texte.downcase.scan(/\w+/)
  frequences = mots.tally
  frequences.max_by { |_mot, count| count }
end

mot, count = mot_frequent("le chat mange le poisson et le chat dort")
puts "#{mot} apparaît #{count} fois"

Section 13.3.1 : SQL fondamental et schema design

🎯 Objectif pédagogique

Maîtriser les fondamentaux de SQL pour créer, interroger et manipuler des bases de données relationnelles. Comprendre la conception de schémas avant de passer à Active Record.


Pourquoi SQL est incontournable

SQL (Structured Query Language) est le langage universel des bases de données relationnelles. Créé en 1970 par IBM, il a survécu à toutes les modes technologiques. Que vous utilisiez PostgreSQL, MySQL, SQLite ou BigQuery, SQL reste la compétence la plus demandée pour tout développeur.

Les bases : CREATE, INSERT, SELECT

-- Créer une table
CREATE TABLE utilisateurs (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  nom VARCHAR(100) NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE,
  age INTEGER,
  ville VARCHAR(100) DEFAULT 'Paris',
  date_inscription DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Insérer des données
INSERT INTO utilisateurs (nom, email, age, ville)
VALUES ('Alice Martin', 'alice@example.com', 28, 'Paris');

INSERT INTO utilisateurs (nom, email, age, ville)
VALUES ('Bob Dupont', 'bob@example.com', 32, 'Lyon');

INSERT INTO utilisateurs (nom, email, age, ville)
VALUES ('Charlie Petit', 'charlie@example.com', 25, 'Paris');

-- Lire des données
SELECT * FROM utilisateurs;
SELECT nom, email FROM utilisateurs;
SELECT nom, age FROM utilisateurs WHERE ville = 'Paris';
SELECT * FROM utilisateurs WHERE age >= 28 ORDER BY age DESC;
SELECT * FROM utilisateurs LIMIT 10 OFFSET 20;  -- Pagination

WHERE : filtrer les données

-- Comparaisons
SELECT * FROM utilisateurs WHERE age > 25;
SELECT * FROM utilisateurs WHERE ville = 'Paris';
SELECT * FROM utilisateurs WHERE nom LIKE 'A%';     -- Commence par A
SELECT * FROM utilisateurs WHERE email LIKE '%@gmail.com';

-- Opérateurs logiques
SELECT * FROM utilisateurs WHERE age > 25 AND ville = 'Paris';
SELECT * FROM utilisateurs WHERE ville = 'Paris' OR ville = 'Lyon';
SELECT * FROM utilisateurs WHERE ville IN ('Paris', 'Lyon', 'Marseille');
SELECT * FROM utilisateurs WHERE age BETWEEN 25 AND 35;
SELECT * FROM utilisateurs WHERE email IS NOT NULL;

Agrégation : GROUP BY et fonctions

-- Fonctions d agrégation
SELECT COUNT(*) FROM utilisateurs;                    -- Nombre total
SELECT AVG(age) FROM utilisateurs;                    -- Âge moyen
SELECT MIN(age), MAX(age) FROM utilisateurs;          -- Min et Max
SELECT SUM(age) FROM utilisateurs WHERE ville = 'Paris';

-- GROUP BY
SELECT ville, COUNT(*) as nb_utilisateurs
FROM utilisateurs
GROUP BY ville
ORDER BY nb_utilisateurs DESC;

-- HAVING (filtre sur les groupes)
SELECT ville, COUNT(*) as nb
FROM utilisateurs
GROUP BY ville
HAVING nb >= 2;

Schema design : relations entre tables

Loading diagram…
-- One-to-Many : un utilisateur a plusieurs articles
CREATE TABLE articles (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  titre VARCHAR(255) NOT NULL,
  contenu TEXT,
  utilisateur_id INTEGER NOT NULL,
  date_publication DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (utilisateur_id) REFERENCES utilisateurs(id)
);

-- Many-to-Many : articles et tags (via table de jonction)
CREATE TABLE tags (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  nom VARCHAR(50) NOT NULL UNIQUE
);

CREATE TABLE articles_tags (
  article_id INTEGER NOT NULL,
  tag_id INTEGER NOT NULL,
  PRIMARY KEY (article_id, tag_id),
  FOREIGN KEY (article_id) REFERENCES articles(id),
  FOREIGN KEY (tag_id) REFERENCES tags(id)
);

JOIN : combiner les tables

-- INNER JOIN : données qui matchent dans les deux tables
SELECT utilisateurs.nom, articles.titre
FROM articles
INNER JOIN utilisateurs ON articles.utilisateur_id = utilisateurs.id;

-- LEFT JOIN : tous les utilisateurs, même sans articles
SELECT utilisateurs.nom, COUNT(articles.id) as nb_articles
FROM utilisateurs
LEFT JOIN articles ON articles.utilisateur_id = utilisateurs.id
GROUP BY utilisateurs.id;

-- Requête complexe
SELECT
  u.nom,
  COUNT(a.id) as nb_articles,
  AVG(LENGTH(a.contenu)) as longueur_moyenne
FROM utilisateurs u
LEFT JOIN articles a ON a.utilisateur_id = u.id
WHERE u.ville = 'Paris'
GROUP BY u.id
HAVING nb_articles > 0
ORDER BY nb_articles DESC
LIMIT 5;

Section 13.3.2 : Migrations et Active Record ORM

🎯 Objectif pédagogique

Comprendre Active Record, l'ORM de Rails qui transforme vos tables SQL en classes Ruby. Apprendre à créer et gérer des migrations pour faire évoluer votre schéma de base de données.


Qu'est-ce qu'un ORM ?

Un ORM (Object-Relational Mapping) fait le pont entre le monde objet (Ruby) et le monde relationnel (SQL). Au lieu d'écrire du SQL brut, vous manipulez des objets Ruby qui correspondent à des tables de la base de données.

# SANS ORM (SQL brut)
db.execute("SELECT * FROM users WHERE age > 25 ORDER BY name")

# AVEC Active Record (Ruby)
User.where('age > ?', 25).order(:name)

# Les deux produisent le même résultat SQL !

Les migrations : versionner votre base

Les migrations sont des fichiers Ruby qui décrivent les modifications de votre schéma de base. Elles permettent de versionner la structure de la DB comme vous versionnez votre code avec Git.

# Générer une migration
rails generate migration CreateUsers nom:string email:string age:integer

# Générer un model (crée le model ET la migration)
rails generate model User nom:string email:string age:integer
# db/migrate/20240315_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :nom, null: false
      t.string :email, null: false
      t.integer :age
      t.string :ville, default: 'Paris'
      t.timestamps  # created_at et updated_at automatiques
    end

    add_index :users, :email, unique: true
  end
end
# Exécuter les migrations
rails db:migrate

# Annuler la dernière migration
rails db:rollback

# Voir le statut
rails db:migrate:status

Ajouter/modifier des colonnes

# Ajouter une colonne
class AddBioToUsers < ActiveRecord::Migration[7.1]
  def change
    add_column :users, :bio, :text
    add_column :users, :avatar_url, :string
  end
end

# Supprimer une colonne
class RemoveVilleFromUsers < ActiveRecord::Migration[7.1]
  def change
    remove_column :users, :ville, :string
  end
end

# Renommer une colonne
class RenameNomToUsername < ActiveRecord::Migration[7.1]
  def change
    rename_column :users, :nom, :username
  end
end

CRUD avec Active Record

# CREATE
user = User.new(nom: 'Alice', email: 'alice@test.com', age: 28)
user.save

# Raccourci
user = User.create(nom: 'Bob', email: 'bob@test.com', age: 32)

# READ
User.all                          # Tous les utilisateurs
User.find(1)                      # Par ID (lève une erreur si absent)
User.find_by(email: 'alice@test.com')  # Par attribut (nil si absent)
User.where(ville: 'Paris')        # Collection filtrée
User.where('age > ?', 25)         # Avec condition SQL
User.order(age: :desc)            # Tri
User.limit(10).offset(20)         # Pagination
User.count                        # Nombre total
User.first                        # Premier
User.last                         # Dernier

# UPDATE
user = User.find(1)
user.update(age: 29)

# ou
user.nom = 'Alice Martin'
user.save

# DELETE
user.destroy
User.destroy_all                  # Tout supprimer

Requêtes avancées Active Record

# Chaîner les requêtes
User.where(ville: 'Paris')
    .where('age > ?', 25)
    .order(nom: :asc)
    .limit(10)

# Agrégation
User.average(:age)               # Âge moyen
User.maximum(:age)               # Âge max
User.minimum(:age)               # Âge min
User.count                       # Nombre total
User.group(:ville).count         # Compter par ville

# Pluck (récupérer une seule colonne)
User.pluck(:email)               # ['alice@test.com', 'bob@test.com']

# Exists?
User.exists?(email: 'alice@test.com')  # true
Loading diagram…

Section 13.3.3 : Associations et validations

🎯 Objectif pédagogique

Maîtriser les associations Active Record (has_many, belongs_to, has_many through) et les validations pour garantir l'intégrité des données.


Les associations : modéliser les relations

Active Record traduit les relations SQL en méthodes Ruby élégantes. Les trois associations les plus courantes :

belongs_to et has_many (One-to-Many)

# Un article appartient à un utilisateur
# Un utilisateur a plusieurs articles

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end

class Article < ApplicationRecord
  belongs_to :user
end
# Utilisation
alice = User.find(1)
alice.articles                    # Tous les articles d Alice
alice.articles.count              # Nombre d articles
alice.articles.create(titre: 'Mon premier article')

article = Article.find(1)
article.user                      # L auteur de l article
article.user.nom                  # Le nom de l auteur

has_many :through (Many-to-Many)

# Un article a plusieurs tags
# Un tag appartient à plusieurs articles

class Article < ApplicationRecord
  has_many :article_tags, dependent: :destroy
  has_many :tags, through: :article_tags
end

class Tag < ApplicationRecord
  has_many :article_tags, dependent: :destroy
  has_many :articles, through: :article_tags
end

class ArticleTag < ApplicationRecord
  belongs_to :article
  belongs_to :tag
end
# Utilisation
article = Article.find(1)
article.tags                       # Tous les tags
article.tags << Tag.find_by(nom: 'Ruby')  # Ajouter un tag

tag = Tag.find_by(nom: 'Rails')
tag.articles                       # Tous les articles avec ce tag

has_one

class User < ApplicationRecord
  has_one :profil, dependent: :destroy
end

class Profil < ApplicationRecord
  belongs_to :user
end

user = User.find(1)
user.profil                        # Le profil unique
user.create_profil(bio: 'Développeuse Ruby')

Les validations : sécuriser les données

Les validations empêchent les données invalides d'être sauvegardées en base. Elles se déclarent dans le model.

class User < ApplicationRecord
  validates :nom, presence: true, length: { minimum: 2, maximum: 50 }
  validates :email, presence: true, uniqueness: true,
                    format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
  validates :age, numericality: { greater_than: 0, less_than: 150 }, allow_nil: true
  validates :role, inclusion: { in: %w[admin editor viewer] }
end
# Tester les validations
user = User.new(nom: '', email: 'invalid')
user.valid?       # false
user.errors.full_messages
# ["Nom can't be blank", "Email is invalid"]

user.save         # false (ne sauvegarde pas)
user.save!        # ActiveRecord::RecordInvalid (lève une exception)

# Validations personnalisées
class Article < ApplicationRecord
  validate :date_future

  private

  def date_future
    if date_publication.present? && date_publication < Date.today
      errors.add(:date_publication, 'doit être dans le futur')
    end
  end
end

Callbacks : hooks du cycle de vie

class User < ApplicationRecord
  before_validation :normaliser_email
  after_create :envoyer_email_bienvenue
  before_destroy :archiver_donnees

  private

  def normaliser_email
    self.email = email.downcase.strip if email.present?
  end

  def envoyer_email_bienvenue
    UserMailer.welcome(self).deliver_later
  end

  def archiver_donnees
    ArchiveService.archiver(self)
  end
end
Loading diagram…

Section 13.3.4 : Requêtes avancées et optimisation

🎯 Objectif pédagogique

Maîtriser les requêtes SQL avancées (subqueries, window functions) et comprendre l'optimisation des performances avec Active Record (N+1, eager loading, indices).


Le problème N+1 : l'ennemi des performances

Le problème N+1 est le piège le plus courant en Active Record. Il se produit quand vous exécutez 1 requête pour récupérer N objets, puis N requêtes supplémentaires pour leurs associations.

# BAD : N+1 queries (1 + 10 = 11 requêtes)
articles = Article.limit(10)
articles.each do |article|
  puts article.user.nom  # Chaque appel fait une requête SQL !
end

# GOOD : Eager loading (2 requêtes seulement)
articles = Article.includes(:user).limit(10)
articles.each do |article|
  puts article.user.nom  # Déjà chargé en mémoire
end

Eager loading : includes, preload, eager_load

# includes : utilise preload ou eager_load selon le cas
Article.includes(:user, :tags).where(published: true)

# preload : 2+ requêtes séparées
Article.preload(:user).limit(10)
# SELECT * FROM articles LIMIT 10
# SELECT * FROM users WHERE id IN (1, 2, 3, ...)

# eager_load : LEFT OUTER JOIN (une seule requête)
Article.eager_load(:user).where(users: { ville: 'Paris' })
# SELECT articles.*, users.* FROM articles
# LEFT OUTER JOIN users ON users.id = articles.user_id
# WHERE users.ville = 'Paris'

Scopes : requêtes réutilisables

class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
  scope :recent, -> { order(created_at: :desc) }
  scope :by_city, ->(city) { joins(:user).where(users: { ville: city }) }
  scope :popular, -> { where('views_count > ?', 100) }

  # Equivalent à des méthodes de classe
  def self.trending
    published.recent.popular.limit(10)
  end
end

# Chaînable
Article.published.recent.limit(5)
Article.by_city('Paris').published
Article.trending

Indices : accélérer les requêtes

Un index est comme l'index d'un livre — il permet à la base de trouver rapidement une ligne sans scanner toute la table.

class AddIndicesToArticles < ActiveRecord::Migration[7.1]
  def change
    add_index :articles, :user_id           # Foreign key
    add_index :articles, :published          # Colonnes fréquemment filtrées
    add_index :articles, :created_at         # Colonnes triées
    add_index :articles, [:user_id, :published]  # Index composite
    add_index :users, :email, unique: true   # Index unique
  end
end

Subqueries et SQL avancé

# Subquery : trouver les users qui ont au moins un article
User.where(id: Article.select(:user_id).distinct)

# SQL brut quand nécessaire
User.find_by_sql("SELECT users.*, COUNT(articles.id) as articles_count
                   FROM users
                   LEFT JOIN articles ON articles.user_id = users.id
                   GROUP BY users.id
                   HAVING COUNT(articles.id) > 5")

# select pour ajouter des colonnes calculées
User.select('users.*, COUNT(articles.id) as articles_count')
    .joins(:articles)
    .group('users.id')
    .order('articles_count DESC')

Section 13.4.1 : HTML/CSS avancé et Bootstrap

🎯 Objectif pédagogique

Maîtriser HTML sémantique, CSS avancé (Flexbox, Grid) et Bootstrap pour construire des interfaces web responsive et professionnelles rapidement.


HTML sémantique

L'HTML sémantique utilise des balises qui décrivent le sens du contenu, pas juste son apparence. Cela améliore l'accessibilité, le SEO et la maintenabilité du code.

<!-- NON sémantique -->
<div class="header">
  <div class="nav">...</div>
</div>
<div class="main">
  <div class="article">...</div>
  <div class="sidebar">...</div>
</div>
<div class="footer">...</div>

<!-- Sémantique -->
<header>
  <nav>...</nav>
</header>
<main>
  <article>...</article>
  <aside>...</aside>
</main>
<footer>...</footer>

Flexbox : aligner en une dimension

Flexbox gère l'alignement sur un axe (horizontal OU vertical) :

.container {
  display: flex;
  justify-content: space-between;  /* Axe principal */
  align-items: center;              /* Axe croisé */
  gap: 16px;                        /* Espacement */
}

/* Direction */
.container { flex-direction: row; }     /* Défaut : horizontal */
.container { flex-direction: column; }  /* Vertical */

/* Wrap */
.container { flex-wrap: wrap; }         /* Retour à la ligne */

/* Flex sur les enfants */
.item { flex: 1; }           /* Prendre l espace disponible */
.item { flex: 0 0 200px; }   /* Taille fixe 200px, pas de grow */

CSS Grid : layouts en deux dimensions

Grid gère les layouts en lignes ET colonnes simultanément :

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);  /* 3 colonnes égales */
  grid-template-rows: auto;
  gap: 24px;
}

/* Responsive */
.grid {
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}

/* Placement explicite */
.hero {
  grid-column: 1 / -1;  /* Toute la largeur */
}

.sidebar {
  grid-row: 2 / 4;       /* Span 2 lignes */
}

Bootstrap : prototyper rapidement

Bootstrap est le framework CSS le plus populaire. Il fournit un système de grille, des composants prêts à l'emploi et des utilitaires CSS qui permettent de construire une interface en quelques minutes.

<!-- Grille responsive Bootstrap -->
<div class="container">
  <div class="row">
    <div class="col-12 col-md-8">
      <h1>Contenu principal</h1>
    </div>
    <div class="col-12 col-md-4">
      <h2>Sidebar</h2>
    </div>
  </div>
</div>

<!-- Card Bootstrap -->
<div class="card" style="width: 18rem;">
  <img src="image.jpg" class="card-img-top" alt="...">
  <div class="card-body">
    <h5 class="card-title">Titre</h5>
    <p class="card-text">Description...</p>
    <a href="#" class="btn btn-primary">Action</a>
  </div>
</div>

<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <div class="container">
    <a class="navbar-brand" href="#">MonApp</a>
    <button class="navbar-toggler" type="button"
            data-bs-toggle="collapse" data-bs-target="#navbarNav">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarNav">
      <ul class="navbar-nav">
        <li class="nav-item">
          <a class="nav-link active" href="#">Accueil</a>
        </li>
      </ul>
    </div>
  </div>
</nav>

Section 13.4.2 : JavaScript ES6+ fondamentaux

🎯 Objectif pédagogique

Maîtriser les fondamentaux de JavaScript moderne (ES6+) : variables, fonctions fléchées, template literals, destructuring, modules, et les méthodes d'array qui sont le cœur du JS quotidien.


JavaScript : le langage du web

JavaScript est le seul langage natif des navigateurs web. Il s'exécute côté client (navigateur) et, depuis Node.js, côté serveur. C'est le langage le plus utilisé au monde selon le Stack Overflow Developer Survey depuis 11 années consécutives.

Variables : let, const, var

// const : valeur qui ne change pas (PRÉFÉRÉ)
const API_URL = 'https://api.example.com';
const MAX_ITEMS = 100;

// let : valeur qui peut changer
let compteur = 0;
compteur += 1;

// var : ancien mot-clé (ÉVITER)
// Portée fonction au lieu de bloc, source de bugs

Types de données

// Primitifs
const nom = 'Alice';          // String
const age = 28;                // Number
const majeur = true;           // Boolean
const rien = null;             // Null
const indefini = undefined;    // Undefined

// Objets
const personne = { nom: 'Alice', age: 28 };
const fruits = ['pomme', 'banane', 'cerise'];

// typeof
typeof nom       // 'string'
typeof age       // 'number'
typeof majeur    // 'boolean'
typeof personne  // 'object'
typeof fruits    // 'object' (les arrays sont des objets)

Template literals

const prenom = 'Alice';
const age = 28;

// Ancien style (concaténation)
const msg1 = 'Bonjour, ' + prenom + ' ! Tu as ' + age + ' ans.';

// Template literal (ES6) — PRÉFÉRÉ
const msg2 = `Bonjour, ${prenom} ! Tu as ${age} ans.`;

// Multi-ligne possible
const html = `
  <div class="card">
    <h2>${prenom}</h2>
    <p>Âge : ${age}</p>
  </div>
`;

Arrow functions

// Fonction classique
function addition(a, b) {
  return a + b;
}

// Arrow function (ES6)
const addition = (a, b) => a + b;

// Avec un seul paramètre, pas besoin de parenthèses
const double = n => n * 2;

// Avec corps multi-ligne, return explicite requis
const saluer = (nom) => {
  const message = `Bonjour, ${nom} !`;
  return message;
};

Destructuring

// Destructuring d objet
const personne = { nom: 'Alice', age: 28, ville: 'Paris' };
const { nom, age, ville } = personne;
console.log(nom);  // 'Alice'

// Avec renommage
const { nom: prenom, age: annees } = personne;

// Destructuring d array
const couleurs = ['rouge', 'vert', 'bleu'];
const [premiere, deuxieme, troisieme] = couleurs;

// Spread operator
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];  // [1, 2, 3, 4, 5]

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 };  // { a: 1, b: 2, c: 3 }

Méthodes d'array (le cœur de JS)

const nombres = [1, 2, 3, 4, 5];

// map : transformer chaque élément
nombres.map(n => n * 2);         // [2, 4, 6, 8, 10]

// filter : garder les éléments qui passent un test
nombres.filter(n => n > 3);      // [4, 5]

// reduce : réduire à une seule valeur
nombres.reduce((acc, n) => acc + n, 0);  // 15

// find : premier élément qui passe le test
nombres.find(n => n > 3);        // 4

// some / every
nombres.some(n => n > 4);        // true
nombres.every(n => n > 0);       // true

// forEach : exécuter sans retour
nombres.forEach(n => console.log(n));

// Chaînage
const resultat = nombres
  .filter(n => n > 2)
  .map(n => n * 10)
  .reduce((acc, n) => acc + n, 0);  // 120

Section 13.4.3 : DOM manipulation et AJAX

🎯 Objectif pédagogique

Comprendre le DOM (Document Object Model), manipuler dynamiquement les pages web avec JavaScript, et communiquer avec un serveur via AJAX (fetch API).


Le DOM : l'arbre HTML vivant

Le DOM est la représentation en mémoire de votre page HTML sous forme d'arbre d'objets. JavaScript peut lire et modifier cet arbre en temps réel, ce qui permet de créer des interfaces dynamiques sans recharger la page.

Loading diagram…

Sélectionner des éléments

// Par ID (un seul élément)
const header = document.getElementById('main-header');

// Par sélecteur CSS (premier match)
const btn = document.querySelector('.btn-primary');
const nav = document.querySelector('nav.main-nav');

// Par sélecteur CSS (tous les matchs)
const items = document.querySelectorAll('.list-item');
const links = document.querySelectorAll('a[href^="https"]');

// Itérer sur une NodeList
items.forEach(item => {
  console.log(item.textContent);
});

Modifier le DOM

// Texte et HTML
element.textContent = 'Nouveau texte';
element.innerHTML = '<strong>HTML</strong> contenu';

// Classes CSS
element.classList.add('active');
element.classList.remove('hidden');
element.classList.toggle('open');
element.classList.contains('active');  // true/false

// Attributs
element.setAttribute('data-id', '42');
element.getAttribute('href');
element.removeAttribute('disabled');

// Styles inline (éviter si possible)
element.style.color = 'red';
element.style.display = 'none';

// Créer et insérer des éléments
const card = document.createElement('div');
card.classList.add('card');
card.innerHTML = `
  <h3>${titre}</h3>
  <p>${description}</p>
`;
document.querySelector('.container').appendChild(card);

// Supprimer
element.remove();

Les événements

// Click
const btn = document.querySelector('#submit-btn');
btn.addEventListener('click', (event) => {
  event.preventDefault();  // Empêcher le comportement par défaut
  console.log('Bouton cliqué !');
});

// Clavier
document.addEventListener('keydown', (event) => {
  if (event.key === 'Escape') {
    closeModal();
  }
});

// Formulaire
const form = document.querySelector('#login-form');
form.addEventListener('submit', (event) => {
  event.preventDefault();
  const formData = new FormData(form);
  const email = formData.get('email');
  const password = formData.get('password');
  // Envoyer au serveur...
});

// Input en temps réel
const search = document.querySelector('#search-input');
search.addEventListener('input', (event) => {
  const query = event.target.value;
  filterResults(query);
});

Event Delegation

Au lieu d'attacher un listener à chaque élément d'une liste, attachez-en un seul au parent :

// MAUVAIS : un listener par bouton
document.querySelectorAll('.delete-btn').forEach(btn => {
  btn.addEventListener('click', handleDelete);
});

// BON : event delegation
document.querySelector('.items-list').addEventListener('click', (event) => {
  if (event.target.matches('.delete-btn')) {
    const itemId = event.target.dataset.id;
    deleteItem(itemId);
  }
});

Fetch API : AJAX moderne

// GET request
const fetchUsers = async () => {
  const response = await fetch('https://api.example.com/users');
  const users = await response.json();
  return users;
};

// POST request
const createUser = async (userData) => {
  const response = await fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(userData),
  });
  const newUser = await response.json();
  return newUser;
};

// Gestion d erreurs
const fetchData = async (url) => {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Erreur fetch:', error);
  }
};

Exemple complet : recherche dynamique

const searchInput = document.querySelector('#search');
const resultsList = document.querySelector('#results');

searchInput.addEventListener('input', async (event) => {
  const query = event.target.value.trim();
  if (query.length < 2) {
    resultsList.innerHTML = '';
    return;
  }

  const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
  const results = await response.json();

  resultsList.innerHTML = results
    .map(r => `<li class="result-item">${r.nom}</li>`)
    .join('');
});

Section 13.4.4 : Stimulus et animations

🎯 Objectif pédagogique

Maîtriser Stimulus, le framework JavaScript léger de Hotwire/Rails, et ajouter des animations CSS pour des interfaces fluides et réactives.


Stimulus : le JavaScript façon Rails

Stimulus est un framework JavaScript minimaliste créé par l'équipe Basecamp/Rails. Contrairement à React ou Vue qui prennent le contrôle de toute la page, Stimulus augmente le HTML existant avec du comportement JavaScript. C'est le compagnon idéal de Rails.

Les concepts Stimulus

Stimulus repose sur trois concepts : Controllers, Actions, et Targets.

<!-- HTML avec annotations Stimulus -->
<div data-controller="hello">
  <input data-hello-target="name" type="text" placeholder="Votre nom">
  <button data-action="click->hello#greet">Saluer</button>
  <p data-hello-target="output"></p>
</div>
// controllers/hello_controller.js
import { Controller } from '@hotwired/stimulus'

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

  greet() {
    const name = this.nameTarget.value
    this.outputTarget.textContent = `Bonjour, ${name} !`
  }
}

Controller de toggle (show/hide)

<div data-controller="toggle">
  <button data-action="click->toggle#toggle">Menu</button>
  <div data-toggle-target="content" class="hidden">
    <ul>
      <li>Option 1</li>
      <li>Option 2</li>
    </ul>
  </div>
</div>
// controllers/toggle_controller.js
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
  static targets = ['content']

  toggle() {
    this.contentTarget.classList.toggle('hidden')
  }
}

Lifecycle callbacks

import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
  connect() {
    // Appelé quand le controller est connecté au DOM
    console.log('Controller connecté !')
  }

  disconnect() {
    // Appelé quand l élément est retiré du DOM
    console.log('Controller déconnecté')
  }
}

Animations CSS

/* Transitions CSS */
.card {
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}

/* Animations avec @keyframes */
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.animate-fade-in {
  animation: fadeIn 0.5s ease-out forwards;
}

/* Loader spinner */
@keyframes spin {
  to { transform: rotate(360deg); }
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #e2e8f0;
  border-top-color: #7C3AED;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

Animations avec Stimulus

// controllers/animation_controller.js
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
  static targets = ['item']

  connect() {
    this.observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            entry.target.classList.add('animate-fade-in')
          }
        })
      },
      { threshold: 0.1 }
    )

    this.itemTargets.forEach(item => {
      this.observer.observe(item)
    })
  }

  disconnect() {
    this.observer.disconnect()
  }
}

Section 13.4.5 : Projet frontend complet

🎯 Objectif pédagogique

Mettre en pratique HTML, CSS, JavaScript, Bootstrap et Stimulus en construisant un projet frontend complet — une application de gestion de tâches avec interface responsive et interactions dynamiques.


Le projet : TaskMaster

Vous allez construire une application de gestion de tâches avec les fonctionnalités suivantes :

  • Interface responsive (mobile-first)
  • Ajout, suppression, et marquage de tâches
  • Filtrage (toutes, actives, complétées)
  • Persistance locale (localStorage)
  • Animations fluides

Structure HTML

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TaskMaster</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container py-5" data-controller="tasks">
    <header class="text-center mb-4">
      <h1>TaskMaster</h1>
      <p class="text-muted">Gérez vos tâches efficacement</p>
    </header>

    <div class="row justify-content-center">
      <div class="col-12 col-md-8 col-lg-6">
        <form data-action="submit->tasks#add" class="mb-4">
          <div class="input-group">
            <input type="text" class="form-control"
                   data-tasks-target="input"
                   placeholder="Nouvelle tâche...">
            <button class="btn btn-primary" type="submit">Ajouter</button>
          </div>
        </form>

        <div class="btn-group w-100 mb-3">
          <button class="btn btn-outline-secondary active"
                  data-action="click->tasks#filterAll">Toutes</button>
          <button class="btn btn-outline-secondary"
                  data-action="click->tasks#filterActive">Actives</button>
          <button class="btn btn-outline-secondary"
                  data-action="click->tasks#filterCompleted">Complétées</button>
        </div>

        <ul class="list-group" data-tasks-target="list"></ul>

        <div class="d-flex justify-content-between mt-3">
          <span data-tasks-target="counter" class="text-muted">0 tâche(s)</span>
          <button class="btn btn-sm btn-outline-danger"
                  data-action="click->tasks#clearCompleted">
            Supprimer complétées
          </button>
        </div>
      </div>
    </div>
  </div>
</body>
</html>

Stimulus Controller

// controllers/tasks_controller.js
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
  static targets = ['input', 'list', 'counter']

  connect() {
    this.tasks = JSON.parse(localStorage.getItem('tasks') || '[]')
    this.filter = 'all'
    this.render()
  }

  add(event) {
    event.preventDefault()
    const text = this.inputTarget.value.trim()
    if (!text) return

    this.tasks.push({
      id: Date.now(),
      text,
      completed: false,
      createdAt: new Date().toISOString()
    })

    this.inputTarget.value = ''
    this.save()
    this.render()
  }

  toggle(event) {
    const id = Number(event.currentTarget.dataset.id)
    const task = this.tasks.find(t => t.id === id)
    if (task) task.completed = !task.completed
    this.save()
    this.render()
  }

  delete(event) {
    const id = Number(event.currentTarget.dataset.id)
    this.tasks = this.tasks.filter(t => t.id !== id)
    this.save()
    this.render()
  }

  filterAll() { this.filter = 'all'; this.render() }
  filterActive() { this.filter = 'active'; this.render() }
  filterCompleted() { this.filter = 'completed'; this.render() }

  clearCompleted() {
    this.tasks = this.tasks.filter(t => !t.completed)
    this.save()
    this.render()
  }

  save() {
    localStorage.setItem('tasks', JSON.stringify(this.tasks))
  }

  render() {
    const filtered = this.tasks.filter(t => {
      if (this.filter === 'active') return !t.completed
      if (this.filter === 'completed') return t.completed
      return true
    })

    this.listTarget.innerHTML = filtered.map(task => `
      <li class="list-group-item d-flex align-items-center ${task.completed ? 'completed' : ''}">
        <input type="checkbox" class="form-check-input me-3"
               ${task.completed ? 'checked' : ''}
               data-id="${task.id}"
               data-action="change->tasks#toggle">
        <span class="flex-grow-1 ${task.completed ? 'text-decoration-line-through text-muted' : ''}">
          ${task.text}
        </span>
        <button class="btn btn-sm btn-outline-danger"
                data-id="${task.id}"
                data-action="click->tasks#delete">X</button>
      </li>
    `).join('')

    const active = this.tasks.filter(t => !t.completed).length
    this.counterTarget.textContent = `${active} tâche(s) restante(s)`
  }
}

Styles CSS

body {
  background: #f8f9fa;
  font-family: 'Inter', sans-serif;
}

h1 {
  background: linear-gradient(135deg, #7C3AED, #a78bfa);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  font-weight: 800;
}

.list-group-item {
  transition: all 0.3s ease;
  border-left: 3px solid transparent;
}

.list-group-item:hover {
  border-left-color: #7C3AED;
  background: #faf5ff;
}

.list-group-item.completed {
  opacity: 0.6;
}

/* Animation d apparition */
.list-group-item {
  animation: slideIn 0.3s ease-out;
}

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateX(-20px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

Section 13.5.1 : Rails MVC — routing et controllers

🎯 Objectif pédagogique

Comprendre l'architecture MVC de Rails, maîtriser le système de routing RESTful et créer des controllers efficaces qui orchestrent la logique de votre application.


Le pattern MVC dans Rails

MVC (Model-View-Controller) est le patron architectural central de Rails. Chaque requête HTTP suit un chemin clair :

Loading diagram…

Le Router : le GPS de Rails

Le router traduit les URLs en actions de controllers. Rails suit la convention REST :

# config/routes.rb

Rails.application.routes.draw do
  # Ressource RESTful complète (7 routes)
  resources :articles

  # Cela génère automatiquement :
  # GET    /articles          => articles#index
  # GET    /articles/new      => articles#new
  # POST   /articles          => articles#create
  # GET    /articles/:id      => articles#show
  # GET    /articles/:id/edit => articles#edit
  # PATCH  /articles/:id      => articles#update
  # DELETE /articles/:id      => articles#destroy

  # Ressources imbriquées
  resources :articles do
    resources :comments, only: [:create, :destroy]
  end
  # => POST /articles/:article_id/comments

  # Route personnalisée
  get '/about', to: 'pages#about'

  # Root route
  root 'pages#home'
end

Les Controllers

Un controller est une classe Ruby qui reçoit les requêtes et coordonne la réponse :

# app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  before_action :set_article, only: [:show, :edit, :update, :destroy]

  def index
    @articles = Article.all.order(created_at: :desc)
  end

  def show
    # @article est défini par before_action
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article, notice: 'Article créé avec succès.'
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    # @article est défini par before_action
  end

  def update
    if @article.update(article_params)
      redirect_to @article, notice: 'Article mis à jour.'
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @article.destroy
    redirect_to articles_path, notice: 'Article supprimé.'
  end

  private

  def set_article
    @article = Article.find(params[:id])
  end

  def article_params
    params.require(:article).permit(:title, :content, :published)
  end
end

Strong Parameters

Rails utilise les Strong Parameters pour se protéger contre l'injection de paramètres (mass assignment) :

# DANGEREUX : accepte tout
@article = Article.new(params[:article])

# SÉCURISÉ : filtre les paramètres autorisés
@article = Article.new(article_params)

private

def article_params
  params.require(:article).permit(:title, :content, :published)
  # Seuls title, content et published sont acceptés
  # Tout autre paramètre (is_admin, user_id...) est ignoré
end

Filtres (Callbacks)

class ApplicationController < ActionController::Base
  # Exécuté avant chaque action
  before_action :authenticate_user!

  # Exécuté après
  after_action :log_activity

  # Conditionnel
  before_action :set_article, only: [:show, :edit, :update, :destroy]
  before_action :check_admin, except: [:index, :show]
end

Section 13.5.2 : Views, partials et helpers

🎯 Objectif pédagogique

Maîtriser le système de vues Rails (ERB), les partials pour la réutilisation, les helpers pour la logique de présentation, et les layouts pour la structure globale des pages.


ERB : Embedded Ruby

ERB est le moteur de templates par défaut de Rails. Il mélange HTML et Ruby :

<!-- app/views/articles/index.html.erb -->

<h1>Articles</h1>

<% @articles.each do |article| %>
  <div class="card mb-3">
    <div class="card-body">
      <h2><%= article.title %></h2>
      <p><%= truncate(article.content, length: 200) %></p>

      <% if article.published? %>
        <span class="badge bg-success">Publié</span>
      <% else %>
        <span class="badge bg-warning">Brouillon</span>
      <% end %>

      <p class="text-muted">
        Publié le <%= article.created_at.strftime('%d/%m/%Y') %>
      </p>

      <%= link_to 'Lire', article_path(article), class: 'btn btn-primary' %>
    </div>
  </div>
<% end %>

Syntaxe ERB :

  • <% code %> — exécute du Ruby (pas d'affichage)
  • <%= expression %> — exécute et affiche le résultat
  • <%# commentaire %> — commentaire (non affiché)

Layouts

Le layout est le squelette HTML qui entoure chaque vue :

<!-- app/views/layouts/application.html.erb -->

<!DOCTYPE html>
<html>
<head>
  <title><%= content_for?(:title) ? yield(:title) : 'MonApp' %></title>
  <%= csrf_meta_tags %>
  <%= stylesheet_link_tag 'application' %>
  <%= javascript_include_tag 'application', defer: true %>
</head>
<body>
  <%= render 'shared/navbar' %>

  <% if notice %>
    <div class="alert alert-success alert-dismissible fade show">
      <%= notice %>
      <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
    </div>
  <% end %>

  <main class="container py-4">
    <%= yield %>
  </main>

  <%= render 'shared/footer' %>
</body>
</html>

Partials : la réutilisation

Les partials sont des fragments de vue réutilisables. Leur nom commence par un underscore :

<!-- app/views/articles/_article.html.erb (partial) -->
<div class="card mb-3" id="<%= dom_id(article) %>">
  <div class="card-body">
    <h3><%= link_to article.title, article %></h3>
    <p><%= truncate(article.content, length: 150) %></p>
    <small class="text-muted"><%= time_ago_in_words(article.created_at) %></small>
  </div>
</div>

<!-- Utilisation dans index.html.erb -->
<%= render @articles %>
<!-- Rails devine automatiquement qu il faut utiliser _article.html.erb -->

<!-- Ou explicitement -->
<%= render partial: 'article', collection: @articles %>

<!-- Partial avec variables locales -->
<%= render 'shared/sidebar', title: 'Navigation', links: @nav_links %>

Formulaires Rails

<!-- app/views/articles/_form.html.erb -->
<%= form_with(model: article, class: 'needs-validation') do |f| %>

  <% if article.errors.any? %>
    <div class="alert alert-danger">
      <h4><%= pluralize(article.errors.count, 'erreur') %></h4>
      <ul>
        <% article.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="mb-3">
    <%= f.label :title, class: 'form-label' %>
    <%= f.text_field :title, class: 'form-control' %>
  </div>

  <div class="mb-3">
    <%= f.label :content, class: 'form-label' %>
    <%= f.text_area :content, class: 'form-control', rows: 10 %>
  </div>

  <div class="mb-3 form-check">
    <%= f.check_box :published, class: 'form-check-input' %>
    <%= f.label :published, 'Publier', class: 'form-check-label' %>
  </div>

  <%= f.submit class: 'btn btn-primary' %>
<% end %>

Helpers

Les helpers contiennent la logique de présentation à réutiliser dans les vues :

# app/helpers/articles_helper.rb

module ArticlesHelper
  def status_badge(article)
    if article.published?
      content_tag(:span, 'Publié', class: 'badge bg-success')
    else
      content_tag(:span, 'Brouillon', class: 'badge bg-warning')
    end
  end

  def reading_time(article)
    words = article.content.split.size
    minutes = (words / 200.0).ceil
    "#{minutes} min de lecture"
  end
end
<!-- Utilisation dans la vue -->
<%= status_badge(@article) %>
<span><%= reading_time(@article) %></span>

Section 13.5.3 : Authentification avec Devise et autorisation Pundit

🎯 Objectif pédagogique

Implémenter un système d'authentification complet avec Devise (inscription, connexion, mot de passe oublié) et gérer les autorisations avec Pundit (qui peut faire quoi).


Devise : l'authentification en 10 minutes

Devise est LA gem d'authentification de Rails. Elle gère inscription, connexion, déconnexion, récupération de mot de passe, confirmation email, et bien plus.

# Installation
bundle add devise
rails generate devise:install
rails generate devise User
rails db:migrate

Cela crée automatiquement :

  • Le modèle User avec email et mot de passe chiffré
  • Les routes de connexion/inscription
  • Les vues de formulaire
  • Les migrations de base de données

Configuration Devise

# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :confirmable, :trackable

  has_many :articles
  has_one :profile
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :authenticate_user!  # Requiert connexion partout

  # Permettre des champs supplémentaires à l inscription
  before_action :configure_permitted_parameters, if: :devise_controller?

  private

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :avatar])
    devise_parameter_sanitizer.permit(:account_update, keys: [:name, :avatar])
  end
end

Helpers Devise dans les vues

<% if user_signed_in? %>
  <span>Bonjour, <%= current_user.name %></span>
  <%= link_to 'Mon profil', edit_user_registration_path %>
  <%= button_to 'Déconnexion', destroy_user_session_path, method: :delete %>
<% else %>
  <%= link_to 'Connexion', new_user_session_path %>
  <%= link_to 'Inscription', new_user_registration_path %>
<% end %>

Pundit : l'autorisation

L'authentification répond à "Qui êtes-vous ?". L'autorisation répond à "Que pouvez-vous faire ?". Pundit gère cette seconde question.

bundle add pundit
rails generate pundit:install
# app/policies/article_policy.rb

class ArticlePolicy < ApplicationPolicy
  def index?
    true  # Tout le monde peut voir la liste
  end

  def show?
    true  # Tout le monde peut voir un article
  end

  def create?
    user.present?  # Tout utilisateur connecté
  end

  def update?
    user == record.author || user.admin?
  end

  def destroy?
    user == record.author || user.admin?
  end

  class Scope < Scope
    def resolve
      if user&.admin?
        scope.all
      else
        scope.where(published: true)
      end
    end
  end
end
# Dans le controller
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    authorize @article  # Vérifie ArticlePolicy#show?
  end

  def index
    @articles = policy_scope(Article)  # Filtre via Scope
  end
end

Section 13.5.4 : Action Cable et temps réel

🎯 Objectif pédagogique

Intégrer le temps réel dans une application Rails avec Action Cable (WebSockets) pour créer des fonctionnalités comme le chat en direct, les notifications instantanées et les mises à jour live.


WebSockets vs HTTP classique

HTTP classique est un protocole requête-réponse : le client demande, le serveur répond, la connexion se ferme. WebSocket maintient une connexion permanente et bidirectionnelle entre client et serveur.

Loading diagram…

Action Cable : WebSockets intégré à Rails

Action Cable est la couche WebSocket de Rails. Il s'intègre nativement avec le reste du framework.

# app/channels/chat_channel.rb

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room_id]}"
  end

  def unsubscribed
    # Nettoyage si nécessaire
  end

  def receive(data)
    # Quand le client envoie un message
    message = Message.create!(
      content: data['content'],
      user: current_user,
      room_id: params[:room_id]
    )

    ActionCable.server.broadcast(
      "chat_#{params[:room_id]}",
      {
        content: message.content,
        user: message.user.name,
        created_at: message.created_at.strftime('%H:%M')
      }
    )
  end
end

Côté JavaScript

// app/javascript/channels/chat_channel.js
import consumer from './consumer'

const chatChannel = consumer.subscriptions.create(
  { channel: 'ChatChannel', room_id: 1 },
  {
    connected() {
      console.log('Connecté au chat !')
    },

    disconnected() {
      console.log('Déconnecté du chat')
    },

    received(data) {
      // Appelé quand le serveur broadcast un message
      const messagesDiv = document.querySelector('#messages')
      messagesDiv.insertAdjacentHTML('beforeend', `
        <div class="message">
          <strong>${data.user}</strong>
          <span class="time">${data.created_at}</span>
          <p>${data.content}</p>
        </div>
      `)
      messagesDiv.scrollTop = messagesDiv.scrollHeight
    },

    // Envoyer un message au serveur
    sendMessage(content) {
      this.perform('receive', { content })
    }
  }
)

// Formulaire d envoi
document.querySelector('#message-form').addEventListener('submit', (e) => {
  e.preventDefault()
  const input = e.target.querySelector('input')
  chatChannel.sendMessage(input.value)
  input.value = ''
})

Broadcast depuis un Model

La manière la plus courante d'utiliser Action Cable est de broadcaster depuis un callback de modèle :

# app/models/message.rb

class Message < ApplicationRecord
  belongs_to :user
  belongs_to :room

  after_create_commit :broadcast_message

  private

  def broadcast_message
    ActionCable.server.broadcast(
      "chat_#{room_id}",
      {
        content: content,
        user: user.name,
        html: ApplicationController.render(
          partial: 'messages/message',
          locals: { message: self }
        )
      }
    )
  end
end

Turbo Streams : le temps réel simplifié

Avec Hotwire/Turbo, le temps réel devient encore plus simple :

# app/models/message.rb
class Message < ApplicationRecord
  broadcasts_to :room  # UNE SEULE LIGNE !
end
<!-- app/views/rooms/show.html.erb -->
<%= turbo_stream_from @room %>

<div id="messages">
  <%= render @room.messages %>
</div>

<%= turbo_frame_tag 'new_message' do %>
  <%= render 'messages/form', message: Message.new(room: @room) %>
<% end %>

Turbo Streams vs Action Cable brut

Turbo Streams est une abstraction au-dessus d'Action Cable qui génère automatiquement le HTML côté serveur et le met à jour côté client. Pour la plupart des cas d'usage, Turbo Streams est le choix préféré car il élimine le JavaScript côté client.

Section 13.5.5 : Déploiement sur Heroku

🎯 Objectif pédagogique

Déployer une application Rails sur Heroku, configurer les variables d'environnement, la base de données PostgreSQL, et mettre en place un pipeline CI/CD basique.


Heroku : la plateforme de déploiement Rails

Heroku est une PaaS (Platform as a Service) qui simplifie le déploiement. Un simple git push déploie votre application. C'est la plateforme historique de la communauté Rails.

Préparation du projet

# Gemfile modifications pour production
group :production do
  gem 'pg'            # PostgreSQL en production
end

group :development, :test do
  gem 'sqlite3'       # SQLite en dev
end
# Procfile (à la racine du projet)
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq
release: bundle exec rails db:migrate

Déploiement initial

# 1. Installer Heroku CLI
# 2. Se connecter
heroku login

# 3. Créer l application
heroku create mon-app-rails
# => https://mon-app-rails.herokuapp.com

# 4. Ajouter PostgreSQL
heroku addons:create heroku-postgresql:mini

# 5. Configurer les variables d'environnement
heroku config:set RAILS_MASTER_KEY=$(cat config/master.key)
heroku config:set RAILS_ENV=production

# 6. Déployer
git push heroku main

# 7. Exécuter les migrations
heroku run rails db:migrate

# 8. Seed si nécessaire
heroku run rails db:seed

Variables d'environnement

# Définir des variables
heroku config:set SECRET_KEY_BASE=abc123
heroku config:set SMTP_HOST=smtp.sendgrid.net
heroku config:set SMTP_USERNAME=apikey

# Voir les variables
heroku config

# Utiliser dans Rails
# config/environments/production.rb
config.action_mailer.smtp_settings = {
  address: ENV['SMTP_HOST'],
  user_name: ENV['SMTP_USERNAME'],
  password: ENV['SMTP_PASSWORD']
}

Configuration PostgreSQL en production

# config/database.yml
production:
  adapter: postgresql
  encoding: unicode
  url: <%= ENV['DATABASE_URL'] %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

Commandes Heroku utiles

# Logs en temps réel
heroku logs --tail

# Console Rails en production
heroku run rails console

# Bash dans le dyno
heroku run bash

# Redémarrer
heroku restart

# Échelle
heroku ps:scale web=2 worker=1

# Maintenance mode
heroku maintenance:on
heroku maintenance:off

# Backup de base de données
heroku pg:backups:capture
heroku pg:backups:download

Alternatives à Heroku

Section 13.6.1 : OpenAI API et prompt engineering

🎯 Objectif pédagogique

Intégrer l'API OpenAI dans une application Rails, comprendre les modèles GPT, maîtriser le prompt engineering et construire des fonctionnalités IA concrètes.


L'API OpenAI

L'API OpenAI donne accès aux modèles GPT-4, GPT-3.5 et aux embeddings. C'est l'interface programmatique qui permet à vos applications d'utiliser l'intelligence artificielle.

# Gemfile
gem 'ruby-openai'
# config/initializers/openai.rb
OpenAI.configure do |config|
  config.access_token = ENV.fetch('OPENAI_API_KEY')
  config.organization_id = ENV.fetch('OPENAI_ORG_ID', nil)
end

OPENAI_CLIENT = OpenAI::Client.new

Premier appel API

# app/services/ai_service.rb

class AiService
  def self.chat(prompt, system_message: nil)
    messages = []
    messages << { role: 'system', content: system_message } if system_message
    messages << { role: 'user', content: prompt }

    response = OPENAI_CLIENT.chat(
      parameters: {
        model: 'gpt-4',
        messages: messages,
        temperature: 0.7,
        max_tokens: 1000
      }
    )

    response.dig('choices', 0, 'message', 'content')
  end
end

# Utilisation
result = AiService.chat(
  'Résume cet article en 3 points clés',
  system_message: 'Tu es un assistant de rédaction concis et précis.'
)

Prompt Engineering : l'art du prompt

Le prompt engineering est la compétence de formuler des instructions claires pour obtenir le résultat désiré des LLMs.

# MAUVAIS prompt
AiService.chat('Écris quelque chose sur le marketing')

# BON prompt - structuré
AiService.chat(
  'Rédige 5 titres d email marketing pour une vente flash de 48h ' \
  'sur des chaussures de running. ' \
  'Cible : hommes 25-40 ans, sportifs occasionnels. ' \
  'Ton : urgent mais pas agressif. ' \
  'Format : un titre par ligne, max 60 caractères.',
  system_message: 'Tu es un expert en copywriting email avec 10 ans ' \
    'd expérience en e-commerce sportif.'
)

Les techniques de prompt engineering

# 1. Few-shot prompting (exemples)
prompt = <<~PROMPT
  Classifie le sentiment de ces avis client.

  Exemples :
  "Livraison rapide, produit top !" => POSITIF
  "Trois semaines d attente, inadmissible" => NÉGATIF
  "Le produit est correct, rien de spécial" => NEUTRE

  Classifie :
  "#{avis_client}"
PROMPT

# 2. Chain-of-thought (raisonnement étape par étape)
prompt = <<~PROMPT
  Analyse ce business plan étape par étape :
  1. Identifie le marché cible
  2. Évalue la proposition de valeur
  3. Analyse le modèle de revenus
  4. Identifie les risques
  5. Donne une note de 1 à 10

  Business plan : #{business_plan}

  Raisonne à voix haute avant de donner ta note finale.
PROMPT

# 3. Structured output (JSON)
prompt = <<~PROMPT
  Extrais les informations de cette facture.
  Retourne UNIQUEMENT un JSON valide avec cette structure :
  {
    "numero_facture": "string",
    "date": "YYYY-MM-DD",
    "montant_total": number,
    "lignes": [{"description": "string", "montant": number}]
  }

  Facture : #{texte_facture}
PROMPT

result = AiService.chat(prompt)
data = JSON.parse(result)

Intégrer l'IA dans un Controller Rails

# app/controllers/ai_summaries_controller.rb

class AiSummariesController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    authorize @article

    summary = AiService.chat(
      "Résume cet article en 3 bullet points de max 20 mots chacun :\n\n#{@article.content}",
      system_message: 'Tu es un assistant de synthèse. Sois concis et précis.'
    )

    @article.update(ai_summary: summary)

    redirect_to @article, notice: 'Résumé IA généré !'
  end
end

Section 13.6.2 : RAG et LangChain

🎯 Objectif pédagogique

Implémenter un système RAG (Retrieval-Augmented Generation) qui permet à un LLM de répondre à des questions en se basant sur vos propres documents, en utilisant LangChain pour orchestrer le pipeline.


Le problème : les LLMs ne connaissent pas vos données

Les modèles GPT ont été entraînés sur des données publiques jusqu'à une date de coupure. Ils ne connaissent pas :

  • Vos documents internes
  • Votre base de connaissances
  • Vos dernières données

RAG résout ce problème en injectant du contexte pertinent dans le prompt.

Loading diagram…

Le pipeline RAG en 4 étapes

# Étape 1 : Charger et découper les documents
class DocumentProcessor
  CHUNK_SIZE = 500
  CHUNK_OVERLAP = 50

  def self.chunk(text)
    words = text.split
    chunks = []
    i = 0
    while i < words.length
      chunk = words[i, CHUNK_SIZE].join(' ')
      chunks << chunk
      i += CHUNK_SIZE - CHUNK_OVERLAP
    end
    chunks
  end
end

# Étape 2 : Générer les embeddings
class EmbeddingService
  def self.generate(text)
    response = OPENAI_CLIENT.embeddings(
      parameters: {
        model: 'text-embedding-3-small',
        input: text
      }
    )
    response.dig('data', 0, 'embedding')
  end
end

# Étape 3 : Stocker dans une base vectorielle
class VectorStore
  def self.store(chunk, embedding, metadata = {})
    Document.create!(
      content: chunk,
      embedding: embedding,
      metadata: metadata
    )
  end

  def self.search(query_embedding, limit: 5)
    Document.nearest_neighbors(:embedding, query_embedding, distance: :cosine)
            .first(limit)
  end
end

# Étape 4 : Générer la réponse
class RagService
  def self.answer(question)
    # 1. Embed la question
    query_embedding = EmbeddingService.generate(question)

    # 2. Chercher les documents pertinents
    relevant_docs = VectorStore.search(query_embedding, limit: 5)
    context = relevant_docs.map(&:content).join("\n\n---\n\n")

    # 3. Construire le prompt enrichi
    prompt = <<~PROMPT
      Contexte (documents internes) :
      #{context}

      Question : #{question}

      Réponds en te basant UNIQUEMENT sur le contexte fourni.
      Si le contexte ne contient pas la réponse, dis-le.
    PROMPT

    # 4. Appeler le LLM
    AiService.chat(prompt, system_message: 'Tu es un assistant qui répond en se basant uniquement sur le contexte fourni.')
  end
end

LangChain : orchestrer les pipelines IA

LangChain est un framework qui simplifie la construction de pipelines IA complexes. Bien que principalement Python, le concept s'applique en Ruby :

# Exemple LangChain en Python (concept)
from langchain.document_loaders import DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.llms import OpenAI

# 1. Charger les documents
loader = DirectoryLoader('./docs/', glob="**/*.md")
docs = loader.load()

# 2. Découper en chunks
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50
)
chunks = splitter.split_documents(docs)

# 3. Créer le vector store
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(chunks, embeddings)

# 4. Créer la chaîne RAG
qa_chain = RetrievalQA.from_chain_type(
    llm=OpenAI(model_name="gpt-4"),
    chain_type="stuff",
    retriever=vectorstore.as_retriever(search_kwargs={"k": 5})
)

# 5. Poser des questions
answer = qa_chain.run("Quelle est la politique de remboursement ?")

Section 13.6.3 : Base de données vectorielle et embeddings

🎯 Objectif pédagogique

Comprendre les embeddings vectoriels, les bases de données vectorielles (pgvector, Pinecone), et implémenter une recherche sémantique performante dans une application Rails.


Qu'est-ce qu'un embedding ?

Un embedding est une représentation numérique d'un texte sous forme de vecteur (liste de nombres). Les textes sémantiquement proches ont des vecteurs proches dans l'espace vectoriel.

# "chat" et "félin" sont proches dans l'espace vectoriel
embedding_chat   = EmbeddingService.generate("Le chat dort")
# => [0.023, -0.041, 0.089, ..., 0.012]  (1536 dimensions)

embedding_felin  = EmbeddingService.generate("Le félin sommeille")
# => [0.025, -0.039, 0.091, ..., 0.010]  (très similaire !)

embedding_voiture = EmbeddingService.generate("La voiture roule")
# => [-0.067, 0.082, -0.015, ..., 0.044]  (très différent)

Similarité cosinus

La similarité cosinus mesure l'angle entre deux vecteurs. Plus ils pointent dans la même direction, plus les textes sont similaires :

def cosine_similarity(vec_a, vec_b)
  dot_product = vec_a.zip(vec_b).sum { |a, b| a * b }
  magnitude_a = Math.sqrt(vec_a.sum { |a| a ** 2 })
  magnitude_b = Math.sqrt(vec_b.sum { |b| b ** 2 })
  dot_product / (magnitude_a * magnitude_b)
end

# "chat" vs "félin" => ~0.95 (très similaire)
# "chat" vs "voiture" => ~0.15 (pas similaire)

pgvector : les vecteurs dans PostgreSQL

pgvector est une extension PostgreSQL qui ajoute un type vector et des index de recherche vectorielle. C'est la solution la plus simple pour Rails.

# Migration Rails
class AddVectorToDocuments < ActiveRecord::Migration[7.0]
  def up
    enable_extension 'vector'
    add_column :documents, :embedding, :vector, limit: 1536
    add_index :documents, :embedding,
              using: :ivfflat,
              opclass: :vector_cosine_ops
  end
end

# Gemfile
gem 'neighbor'  # Gem Rails pour pgvector
# app/models/document.rb
class Document < ApplicationRecord
  has_neighbors :embedding

  # Recherche les documents les plus similaires
  scope :search_similar, ->(query_embedding, limit: 5) {
    nearest_neighbors(:embedding, query_embedding, distance: :cosine)
      .first(limit)
  }
end

Pipeline complet de recherche sémantique

# app/services/semantic_search.rb

class SemanticSearch
  def self.index_document(document)
    chunks = DocumentProcessor.chunk(document.content)

    chunks.each_with_index do |chunk, index|
      embedding = EmbeddingService.generate(chunk)

      DocumentChunk.create!(
        document: document,
        content: chunk,
        chunk_index: index,
        embedding: embedding
      )
    end
  end

  def self.search(query, limit: 5)
    query_embedding = EmbeddingService.generate(query)

    DocumentChunk
      .nearest_neighbors(:embedding, query_embedding, distance: :cosine)
      .first(limit)
      .map do |chunk|
        {
          content: chunk.content,
          document: chunk.document,
          similarity: chunk.neighbor_distance
        }
      end
  end
end

# Utilisation
SemanticSearch.index_document(article)
results = SemanticSearch.search("Comment configurer l authentification ?")

Solutions de vector DB

Section 13.6.4 : AI-assisted coding et Cursor IDE

🎯 Objectif pédagogique

Maîtriser les outils d'IA pour le développement : GitHub Copilot, Cursor IDE, et les techniques pour maximiser la productivité avec l'assistance IA tout en gardant le contrôle du code.


L'ère du développement assisté par IA

L'IA transforme le métier de développeur. Les outils d'assistance IA ne remplacent pas le développeur — ils amplifient ses capacités. Les études montrent des gains de productivité de 30-55% sur certaines tâches.

GitHub Copilot

GitHub Copilot est l'outil d'autocomplétion IA le plus utilisé. Il s'intègre directement dans VS Code et suggère du code en temps réel.

# Tapez un commentaire descriptif, Copilot génère le code

# Valide un email avec regex
def valid_email?(email)
  email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end

# Convertit un prix en centimes
def price_in_cents(price_string)
  (price_string.to_f * 100).round
end

# Génère un slug URL-friendly à partir d un titre
def slugify(title)
  title.downcase.strip.gsub(/[^\w\s-]/, '').gsub(/[\s_]+/, '-')
end

Cursor IDE : l'IDE IA-natif

Cursor est un fork de VS Code entièrement repensé pour l'IA. Il va au-delà de l'autocomplétion avec des fonctionnalités de refactoring, débogage et génération de code à grande échelle.

Fonctionnalités clés de Cursor :

  1. Cmd+K (Edit) : Sélectionnez du code et décrivez la modification
  2. Cmd+L (Chat) : Chat avec le contexte de votre codebase
  3. @ mentions : Référencez des fichiers, docs, ou URLs dans le chat
  4. Composer : Modifiez plusieurs fichiers simultanément
  5. Tab completion : Autocomplétion contextuelle avancée

Bonnes pratiques avec l'IA

# 1. Commentaires comme spécifications
# Service qui envoie un email de bienvenue
# - Vérifie que l'utilisateur a un email confirmé
# - Utilise le template 'welcome' avec le nom de l'utilisateur
# - Envoie de manière asynchrone via Sidekiq
# - Log le résultat dans le journal d'activité
class WelcomeEmailService
  # L'IA génère le code basé sur cette spec...
end

# 2. Tests d abord (TDD + IA)
# Écrivez le test, l'IA génère l'implémentation
RSpec.describe WelcomeEmailService do
  it 'sends welcome email to confirmed user' do
    user = create(:user, email_confirmed: true)
    expect { WelcomeEmailService.call(user) }
      .to have_enqueued_job(ActionMailer::MailDeliveryJob)
  end

  it 'does not send to unconfirmed user' do
    user = create(:user, email_confirmed: false)
    expect { WelcomeEmailService.call(user) }
      .not_to have_enqueued_job
  end
end

Ce que l'IA fait bien vs mal

Règle d or de l IA assistée

Ne committez JAMAIS du code IA que vous ne comprenez pas à 100%. L'IA est un copilote, pas le pilote. Vous êtes responsable de chaque ligne de code dans votre application. Relisez, testez, et comprenez avant de merger.

Section 13.6.5 : Agents IA et orchestration

🎯 Objectif pédagogique

Comprendre les agents IA autonomes, l'orchestration multi-agents, et implémenter un agent capable d'utiliser des outils (function calling) pour accomplir des tâches complexes.


Qu'est-ce qu'un agent IA ?

Un agent IA est un système qui utilise un LLM pour raisonner, planifier, et agir de manière autonome. Contrairement à un simple chatbot qui répond, un agent peut utiliser des outils, consulter des bases de données, et exécuter des actions.

Loading diagram…

Function Calling (Tool Use)

Le function calling permet au LLM d'appeler des fonctions que vous définissez :

# Définir les outils disponibles
TOOLS = [
  {
    type: 'function',
    function: {
      name: 'search_products',
      description: 'Recherche des produits dans le catalogue',
      parameters: {
        type: 'object',
        properties: {
          query: { type: 'string', description: 'Termes de recherche' },
          category: { type: 'string', description: 'Catégorie de produit' },
          max_price: { type: 'number', description: 'Prix maximum' }
        },
        required: ['query']
      }
    }
  },
  {
    type: 'function',
    function: {
      name: 'get_order_status',
      description: 'Vérifie le statut d une commande',
      parameters: {
        type: 'object',
        properties: {
          order_id: { type: 'string', description: 'ID de la commande' }
        },
        required: ['order_id']
      }
    }
  }
]

Implémentation d'un agent

# app/services/agent_service.rb

class AgentService
  MAX_ITERATIONS = 10

  def initialize(tools:)
    @tools = tools
    @messages = []
  end

  def run(user_message)
    @messages << { role: 'user', content: user_message }

    MAX_ITERATIONS.times do
      response = OPENAI_CLIENT.chat(
        parameters: {
          model: 'gpt-4',
          messages: @messages,
          tools: @tools,
          tool_choice: 'auto'
        }
      )

      message = response.dig('choices', 0, 'message')
      @messages << message

      # Si le LLM veut appeler un outil
      if message['tool_calls']
        message['tool_calls'].each do |tool_call|
          result = execute_tool(
            tool_call['function']['name'],
            JSON.parse(tool_call['function']['arguments'])
          )

          @messages << {
            role: 'tool',
            tool_call_id: tool_call['id'],
            content: result.to_json
          }
        end
      else
        # Réponse finale
        return message['content']
      end
    end

    'Agent a atteint la limite d itérations.'
  end

  private

  def execute_tool(name, args)
    case name
    when 'search_products'
      Product.search(args['query'])
             .where(category: args['category'])
             .where('price <= ?', args['max_price'])
             .limit(5)
             .map { |p| { name: p.name, price: p.price } }
    when 'get_order_status'
      order = Order.find_by(id: args['order_id'])
      order ? { status: order.status, eta: order.eta } : { error: 'Commande non trouvée' }
    end
  end
end

# Utilisation
agent = AgentService.new(tools: TOOLS)
response = agent.run(
  "Je cherche des chaussures de running sous 100€, "  \
  "et aussi le statut de ma commande #12345"
)

Orchestration multi-agents

Pour des tâches complexes, plusieurs agents spécialisés collaborent :

# Agent de routage (dispatcher)
class DispatcherAgent
  AGENTS = {
    'sales' => SalesAgent,
    'support' => SupportAgent,
    'analytics' => AnalyticsAgent
  }

  def route(message)
    # Le LLM détermine quel agent doit traiter
    category = AiService.chat(
      "Classifie ce message en une catégorie : sales, support, analytics.\n" \
      "Message : #{message}\n" \
      "Réponds uniquement par la catégorie.",
      system_message: 'Tu es un routeur de messages.'
    ).strip.downcase

    agent_class = AGENTS[category]
    agent_class.new.handle(message)
  end
end

Section 13.7.1 : Conception et architecture du projet final

🎯 Objectif pédagogique

Concevoir l'architecture complète d'une application web Rails intégrant de l'IA : user stories, modèle de données, wireframes, et choix technologiques argumentés.


Le projet final : votre vitrine professionnelle

Le projet final du bootcamp est une application full-stack Rails avec intégration IA que vous concevez et réalisez de A à Z. C'est votre démonstration de compétences pour les recruteurs et clients.

Exemples de projets :

  • Assistant juridique IA : RAG sur des textes de loi, chat avec un avocat virtuel
  • Plateforme de recrutement : analyse de CV par IA, matching candidat/offre
  • Dashboard analytics : visualisation de données avec résumés IA automatiques
  • E-commerce intelligent : recommandations produits, descriptions auto-générées

Phase 1 : User Stories

# Exemple de user stories pour un assistant juridique

En tant qu'utilisateur, je veux :
- [ ] M'inscrire et me connecter à l'application
- [ ] Poser une question juridique en langage naturel
- [ ] Recevoir une réponse sourcée (articles de loi cités)
- [ ] Consulter l'historique de mes conversations
- [ ] Sauvegarder des réponses en favoris

En tant qu'administrateur, je veux :
- [ ] Ajouter/supprimer des documents juridiques
- [ ] Voir les analytics d'utilisation
- [ ] Gérer les utilisateurs
- [ ] Modérer les contenus IA inappropriés

Phase 2 : Modèle de données

Loading diagram…

Phase 3 : Architecture technique

## Stack technique

- **Backend** : Ruby on Rails 7.1
- **Base de données** : PostgreSQL + pgvector
- **Frontend** : Hotwire (Turbo + Stimulus) + Bootstrap 5
- **IA** : OpenAI API (GPT-4 + text-embedding-3-small)
- **Background jobs** : Sidekiq + Redis
- **Déploiement** : Heroku (ou Render)
- **Tests** : RSpec + FactoryBot

## APIs externes
- OpenAI (chat + embeddings)
- SendGrid (emails)
- Sentry (monitoring erreurs)

Phase 4 : Wireframes

Avant de coder, dessinez vos écrans. Utilisez Figma, Excalidraw, ou même papier/crayon. L'objectif : valider le flux utilisateur avant d'écrire la première ligne de code.

Pages clés à wireframer :

  1. Page d'accueil / Landing
  2. Inscription / Connexion
  3. Dashboard (interface principale)
  4. Interface de chat IA
  5. Page de résultats / historique
  6. Panel administration

Section 13.7.2 : Développement full-stack du projet

🎯 Objectif pédagogique

Implémenter le projet final en suivant les bonnes pratiques Rails : développement itératif, TDD, code review, et gestion de projet avec Git flow.


Méthodologie de développement

Le développement suit une approche itérative avec des sprints courts :

## Sprint 1 (3 jours) : Fondations
- [ ] Initialiser le projet Rails
- [ ] Configurer la base de données (PostgreSQL + pgvector)
- [ ] Implémenter l'authentification (Devise)
- [ ] Créer le layout principal (Bootstrap)
- [ ] Déployer sur Heroku (CI/CD)

## Sprint 2 (3 jours) : Fonctionnalités core
- [ ] Modèles et migrations
- [ ] CRUD principal
- [ ] Interface utilisateur (Turbo + Stimulus)
- [ ] Tests RSpec

## Sprint 3 (3 jours) : Intégration IA
- [ ] Service OpenAI
- [ ] Pipeline RAG (embeddings + recherche)
- [ ] Interface de chat
- [ ] Gestion des erreurs et limites

## Sprint 4 (3 jours) : Finitions
- [ ] Administration (Pundit)
- [ ] Responsive design
- [ ] Performance (caching, N+1)
- [ ] Tests E2E

Git Flow pour le projet

# Branches principales
main          # Production (toujours déployable)
develop       # Développement (intégration)

# Branches de feature
git checkout -b feature/user-auth
# ... développer ...
git push origin feature/user-auth
# Créer une Pull Request vers develop

# Convention de commits
git commit -m "feat: add user authentication with Devise"
git commit -m "fix: resolve N+1 query in conversations#index"
git commit -m "test: add RSpec tests for AiService"
git commit -m "docs: update README with setup instructions"

Scaffold intelligent avec Rails

# Générer les modèles rapidement
rails generate model User name:string email:string role:string
rails generate model Conversation user:references title:string
rails generate model Message conversation:references role:string content:text tokens_used:integer
rails generate model Document title:string content:text category:string
rails generate model Chunk document:references content:text chunk_index:integer

# Générer un controller complet
rails generate controller Conversations index show create --skip-routes

# Migrations
rails db:migrate

TDD : tester en même temps que vous développez

# spec/services/ai_service_spec.rb

RSpec.describe AiService do
  describe '.chat' do
    it 'returns a response from OpenAI' do
      VCR.use_cassette('openai_chat') do
        result = AiService.chat('Dis bonjour')
        expect(result).to be_a(String)
        expect(result.length).to be > 0
      end
    end

    it 'includes system message when provided' do
      VCR.use_cassette('openai_chat_system') do
        result = AiService.chat(
          'Quel est ton rôle ?',
          system_message: 'Tu es un assistant juridique.'
        )
        expect(result).to include('juridique').or include('droit')
      end
    end
  end
end

# spec/models/conversation_spec.rb
RSpec.describe Conversation do
  it { should belong_to(:user) }
  it { should have_many(:messages).dependent(:destroy) }
  it { should validate_presence_of(:title) }

  describe '#last_message' do
    it 'returns the most recent message' do
      conversation = create(:conversation)
      old_msg = create(:message, conversation: conversation, created_at: 1.hour.ago)
      new_msg = create(:message, conversation: conversation, created_at: Time.current)

      expect(conversation.last_message).to eq(new_msg)
    end
  end
end

Code Review checklist

## Avant chaque Pull Request, vérifiez :

### Fonctionnel
- [ ] La feature fonctionne comme prévu
- [ ] Les edge cases sont gérés
- [ ] Pas de régression sur les features existantes

### Code Quality
- [ ] DRY (pas de duplication)
- [ ] Methods < 10 lignes
- [ ] Nommage explicite (pas de variables x, y, temp)
- [ ] Strong Parameters utilisés
- [ ] N+1 queries éliminées

### Tests
- [ ] Tests unitaires pour les models/services
- [ ] Tests d intégration pour les controllers
- [ ] Tests passent en CI

### Sécurité
- [ ] Input sanitisé
- [ ] Autorisations Pundit en place
- [ ] Pas de secrets dans le code

Section 13.7.3 : Intégration IA avancée

🎯 Objectif pédagogique

Intégrer des fonctionnalités IA avancées dans le projet final : streaming de réponses, gestion de contexte conversationnel, monitoring des coûts, et gestion des erreurs IA.


Streaming des réponses IA

Au lieu d'attendre la réponse complète (5-10 secondes), le streaming affiche les tokens au fur et à mesure, comme ChatGPT :

# app/services/streaming_ai_service.rb

class StreamingAiService
  def self.stream_chat(messages, &block)
    OPENAI_CLIENT.chat(
      parameters: {
        model: 'gpt-4',
        messages: messages,
        stream: proc { |chunk, _bytesize|
          delta = chunk.dig('choices', 0, 'delta', 'content')
          block.call(delta) if delta
        }
      }
    )
  end
end
# Avec Action Cable pour le streaming temps réel
# app/channels/chat_stream_channel.rb

class ChatStreamChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_stream_#{params[:conversation_id]}"
  end

  def ask(data)
    conversation = Conversation.find(params[:conversation_id])
    messages = conversation.messages.map { |m| { role: m.role, content: m.content } }
    messages << { role: 'user', content: data['content'] }

    # Sauvegarder le message utilisateur
    conversation.messages.create!(role: 'user', content: data['content'])

    # Streamer la réponse
    full_response = ''
    StreamingAiService.stream_chat(messages) do |token|
      full_response += token
      ActionCable.server.broadcast(
        "chat_stream_#{conversation.id}",
        { token: token, type: 'stream' }
      )
    end

    # Sauvegarder la réponse complète
    conversation.messages.create!(role: 'assistant', content: full_response)

    ActionCable.server.broadcast(
      "chat_stream_#{conversation.id}",
      { type: 'complete' }
    )
  end
end

Gestion du contexte conversationnel

# app/services/conversation_manager.rb

class ConversationManager
  MAX_CONTEXT_TOKENS = 4000

  def self.build_messages(conversation)
    messages = []

    # System message
    messages << {
      role: 'system',
      content: system_prompt(conversation)
    }

    # Contexte RAG si pertinent
    if conversation.uses_rag?
      context = RagService.get_context(conversation.last_user_message)
      messages << {
        role: 'system',
        content: "Contexte documentaire :\n#{context}"
      }
    end

    # Historique (limité pour respecter la fenêtre de contexte)
    recent_messages = conversation.messages
      .order(created_at: :desc)
      .limit(20)
      .reverse

    recent_messages.each do |msg|
      messages << { role: msg.role, content: msg.content }
    end

    # Tronquer si nécessaire
    truncate_to_token_limit(messages)
  end

  private

  def self.truncate_to_token_limit(messages)
    # Garder le system message et les derniers messages
    while estimate_tokens(messages) > MAX_CONTEXT_TOKENS && messages.length > 2
      messages.delete_at(1)  # Supprimer les plus anciens (garder system)
    end
    messages
  end

  def self.estimate_tokens(messages)
    messages.sum { |m| m[:content].length / 4 }  # Approximation
  end
end

Monitoring des coûts

# app/models/ai_usage.rb

class AiUsage < ApplicationRecord
  belongs_to :user

  # Tracker chaque appel API
  def self.track(user:, model:, input_tokens:, output_tokens:)
    costs = {
      'gpt-4' => { input: 0.03, output: 0.06 },         # par 1K tokens
      'gpt-3.5-turbo' => { input: 0.0015, output: 0.002 },
      'text-embedding-3-small' => { input: 0.00002, output: 0 }
    }

    model_cost = costs[model]
    total_cost = (input_tokens / 1000.0 * model_cost[:input]) +
                 (output_tokens / 1000.0 * model_cost[:output])

    create!(
      user: user,
      model: model,
      input_tokens: input_tokens,
      output_tokens: output_tokens,
      cost_usd: total_cost
    )
  end

  # Limites par utilisateur
  def self.monthly_cost(user)
    where(user: user)
      .where('created_at >= ?', Time.current.beginning_of_month)
      .sum(:cost_usd)
  end
end

Gestion des erreurs IA

# app/services/resilient_ai_service.rb

class ResilientAiService
  MAX_RETRIES = 3
  RETRY_DELAY = 1 # seconde

  def self.chat(prompt, **options)
    retries = 0
    begin
      result = AiService.chat(prompt, **options)
      raise 'Réponse vide' if result.blank?
      result
    rescue Faraday::TooManyRequestsError => e
      # Rate limiting — attendre et réessayer
      retries += 1
      if retries <= MAX_RETRIES
        sleep(RETRY_DELAY * retries)
        retry
      end
      fallback_response('Service temporairement surchargé.')
    rescue Faraday::ServerError => e
      # Erreur serveur OpenAI
      Rails.logger.error("OpenAI server error: #{e.message}")
      fallback_response('Le service IA est temporairement indisponible.')
    rescue JSON::ParserError => e
      Rails.logger.error("Invalid JSON from OpenAI: #{e.message}")
      fallback_response('Erreur de format dans la réponse IA.')
    end
  end

  private

  def self.fallback_response(message)
    "Désolé, #{message} Veuillez réessayer dans quelques instants."
  end
end

Coûts IA en production

Surveillez vos coûts OpenAI dès le début. Un utilisateur qui abuse du chat peut facilement générer 10$/jour en tokens GPT-4. Implémentez des limites quotidiennes/mensuelles par utilisateur et utilisez GPT-3.5-turbo pour les tâches simples (classification, extraction) et GPT-4 uniquement quand la qualité est critique.

Section 13.7.4 : Tests, pitch et démo finale

🎯 Objectif pédagogique

Finaliser le projet avec des tests complets, préparer un pitch professionnel de 10 minutes, et réaliser une démo live convaincante pour les recruteurs ou clients.


Stratégie de tests

# spec/system/conversations_spec.rb (test E2E)

RSpec.describe 'Conversations', type: :system do
  let(:user) { create(:user) }

  before { sign_in user }

  it 'allows user to start a new conversation' do
    visit conversations_path
    click_on 'Nouvelle conversation'

    fill_in 'Votre question', with: 'Quelles sont les règles du RGPD ?'
    click_on 'Envoyer'

    expect(page).to have_content('Quelles sont les règles du RGPD ?')
    expect(page).to have_css('.ai-response', wait: 15)
  end

  it 'saves conversation history' do
    conversation = create(:conversation, user: user, title: 'RGPD')
    create(:message, conversation: conversation, role: 'user', content: 'Question test')
    create(:message, conversation: conversation, role: 'assistant', content: 'Réponse test')

    visit conversation_path(conversation)

    expect(page).to have_content('Question test')
    expect(page).to have_content('Réponse test')
  end
end
# spec/services/rag_service_spec.rb

RSpec.describe RagService do
  describe '.answer' do
    let!(:document) do
      create(:document, content: 'Le RGPD impose une base légale pour tout traitement de données personnelles.')
    end

    before { SemanticSearch.index_document(document) }

    it 'answers based on indexed documents' do
      VCR.use_cassette('rag_answer') do
        answer = RagService.answer('Que dit le RGPD sur les données personnelles ?')
        expect(answer).to include('base légale').or include('données personnelles')
      end
    end

    it 'indicates when no relevant context is found' do
      VCR.use_cassette('rag_no_context') do
        answer = RagService.answer('Comment faire une omelette ?')
        expect(answer).to include('pas').or include('contexte')
      end
    end
  end
end

Métriques de qualité

## Checklist de qualité projet final

### Code
- [ ] Test coverage > 80%
- [ ] Aucune erreur RuboCop
- [ ] Pas de N+1 queries (Bullet gem)
- [ ] Seeds fonctionnels pour démo

### Performance
- [ ] Page load < 2 secondes
- [ ] Réponse IA streaming < 500ms (premier token)
- [ ] Base de données avec index appropriés

### Sécurité
- [ ] Authentification (Devise)
- [ ] Autorisations (Pundit)
- [ ] Strong Parameters partout
- [ ] HTTPS forcé
- [ ] Variables d environnement pour les secrets

### Déploiement
- [ ] CI/CD fonctionnel
- [ ] Heroku (ou alternative) configuré
- [ ] Domain custom (optionnel)
- [ ] Monitoring (Sentry)

Préparer votre pitch

Le pitch est une présentation de 10 minutes qui raconte l'histoire de votre projet :

## Structure du pitch (10 minutes)

### 1. Le problème (2 min)
- Quel problème résolvez-vous ?
- Pour qui ? (persona)
- Pourquoi c est important ? (chiffres)

### 2. La solution (2 min)
- Votre application en une phrase
- Les 3 features clés
- Pourquoi l IA est nécessaire (pas juste cool)

### 3. Démo live (4 min)
- Scénario concret (pas un tour de features)
- Montrer le happy path complet
- Montrer une feature IA impressionnante

### 4. Architecture technique (1 min)
- Diagramme simple
- Technologies clés et pourquoi
- Un défi technique et comment vous l avez résolu

### 5. Conclusion (1 min)
- Ce que vous avez appris
- Prochaines étapes
- Call to action

Préparer la démo

## Checklist démo

### Avant la démo
- [ ] Données de seed chargées et visuellement attrayantes
- [ ] Connexion internet stable (ou démo offline si possible)
- [ ] Un compte de démo pré-créé
- [ ] Screenshots en backup si la démo plante
- [ ] Navigateur en mode présentation (clean, pas d onglets)

### Pendant la démo
- [ ] Raconter une HISTOIRE, pas lister des features
  "Marie est avocate. Elle reçoit 50 questions par jour..."
- [ ] Montrer le problème AVANT la solution
- [ ] Interagir avec l app en temps réel
- [ ] Montrer une réponse IA en streaming (effet wow)
- [ ] Garder le rythme (pas de temps mort)

### Si quelque chose plante
- Restez calme
- "Comme dans tout logiciel en production, des bugs arrivent !"
- Montrez le screenshot de backup
- Passez à la suite

Portfolio et GitHub

## README professionnel pour votre repo

# 🤖 LegalAI — Assistant Juridique Intelligent

> Application web Rails permettant aux professionnels du droit
> de consulter une base juridique via un chat IA (RAG + GPT-4).

## Features
- Chat IA avec réponses sourcées (articles de loi)
- Base documentaire avec recherche sémantique
- Historique de conversations
- Dashboard d analytics

## Tech Stack
- Ruby on Rails 7.1
- PostgreSQL + pgvector
- OpenAI API (GPT-4 + embeddings)
- Hotwire (Turbo + Stimulus)
- Bootstrap 5

## Screenshots
[Screenshots de l app ici]

## Demo
🔗 [Live Demo](https://legalai.herokuapp.com)

## Installation
[Instructions de setup local]

Learn AI — From Prompts to Agents

10 Free Interactive Guides120+ Hands-On Exercises100% Free
GO DEEPER — FREE GUIDE

AI for Marketing & RevOps

120h to master AI for marketing, sales and customer success: intelligent CRM, predictive scoring, multi-channel agents.

Newsletter

Weekly AI Insights

Tools, techniques & news — curated for AI practitioners. Free, no spam.

Free, no spam. Unsubscribe anytime.