Rails 5 est sorti en juin — et on n’a pas attendu pour migrer. Quatre mois plus tard, on boucle le premier gros projet qu’on a construit dessus : Hively, une marketplace de services à domicile. L’app mobile (Ionic) communique avec une API Rails qui gère les prestataires, les disponibilités, les réservations, le chat en temps réel via Action Cable et le back-office.

C’est un projet qu’on connaît bien chez imagine — on avait déjà construit le backend Rails de Simone.paris l’an dernier, une app de réservation de coiffure à domicile, et plusieurs autres projets de ce type depuis. Hively va nettement plus loin : multi-services (ménage, fitness, garde d’enfants, pet-care, beauté), réservations récurrentes, communautés, et un moteur de recherche de prestataires avec calcul de disponibilités en temps réel. On a construit une architecture polymorphique data-driven qui permet d’ajouter un nouveau service sans toucher une ligne de code applicatif — uniquement de la configuration.

Cet article détaille trois aspects de l’architecture backend qui illustrent pourquoi Rails reste, à notre avis, le framework le plus productif pour ce type de projet.

Rails 5 en mode API : le bon timing

Ce qu’on faisait avant — et pourquoi c’était pénible

On utilisait déjà Rails comme backend d’apps mobiles — c’est ce qu’on fait depuis Simone. Mais jusqu’à Rails 4.2, construire une API propre demandait du travail supplémentaire.

Option 1 : Grape. On a testé Grape, le framework Ruby spécialisé API, sur un projet précédent. L’idée est séduisante — un DSL dédié, une documentation Swagger auto-générée, un routage indépendant de Rails. En pratique, on se retrouve avec deux frameworks dans la même app (Grape pour l’API, Rails pour le back-office), deux systèmes de routing, deux façons de gérer les erreurs, et des modèles qui doivent servir les deux. La maintenance devient un casse-tête.

Option 2 : Rails allégé à la main. C’est ce qu’on faisait sur Simone. On partait d’un rails new classique, puis on passait une demi-journée à déshabiller le framework :

# Ce qu'on devait faire en Rails 4.2 — à la main, à chaque projet
config.middleware.delete ActionDispatch::Cookies
config.middleware.delete ActionDispatch::Session::CookieStore
config.middleware.delete ActionDispatch::Flash
config.middleware.delete Rack::MethodOverride

# Supprimer les générateurs de vues
config.generators do |g|
  g.template_engine nil
  g.assets false
  g.helper false
end

# Ignorer l'asset pipeline dans les contrôleurs
# Et prier pour que le prochain gem qu'on installe
# n'assume pas que les vues existent...

Fragile, non documenté, et chaque gem tierce pouvait casser en assumant la présence des middlewares qu’on avait supprimés. ActiveAdmin, par exemple, avait besoin des sessions — il fallait les réactiver chirurgicalement pour le namespace /admin.

Option 3 : rails-api. La gem rails-api de Santiago Pastorino — un précurseur du mode API officiel. On l’a utilisée brièvement, mais la gem restait un patch non officiel sur Rails, avec un risque de compatibilité à chaque montée de version.

Rails 5 : enfin officiel

Rails 5 met fin à ce bricolage. rails new hively --api génère un projet allégé nativement :

# application.rb — Rails 5 API mode
module Hively
  class Application < Rails::Application
    config.api_only = true
  end
end

Ce flag supprime les middlewares de session, de cookies, de CSRF, de flash — tout ce qui concerne le rendu HTML. Le contrôleur de base hérite de ActionController::API au lieu de ActionController::Base. Le projet démarre léger, et on n’ajoute que ce dont on a besoin.

En pratique, on a quand même besoin de sessions pour le back-office ActiveAdmin — on réactive les middlewares cookie/session pour le namespace /admin uniquement. Le reste de l’app est purement stateless, avec une authentification par token Bearer.

L’authentification par token

On a construit un système de tokens custom plutôt que d’utiliser Devise pour l’API. Devise est génial pour l’authentification web (on l’utilise pour les prestataires et le back-office), mais pour une API mobile, un token simple et robuste suffit :

# app/models/access_token.rb
class AccessToken < ApplicationRecord
  ACCESS_TOKEN_EXPIRATION_DELAY = 3.months

  belongs_to :authenticatable, polymorphic: true

  scope :active, -> {
    where(canceled_at: nil)
      .where('expires_at > ?', Time.current)
  }

  before_create :generate_token

  def self.find_by_access_token(clear_token)
    token = active.find_by(
      token_digest: Digest::SHA256.hexdigest(clear_token)
    )
    token&.touch(:last_accessed_at)
    token
  end

  private

  def generate_token
    clear_token = SecureRandom.urlsafe_base64(96)
    self.token_digest = Digest::SHA256.hexdigest(clear_token)
    self.clear_token = clear_token  # attribut virtuel, renvoyé une seule fois
    self.expires_at = ACCESS_TOKEN_EXPIRATION_DELAY.from_now
  end
end
CYCLE DE VIE DU TOKEN D’AUTHENTIFICATIONLe token clair n’existe qu’à la création — seul le digest SHA-256 est persisté1. POST /loginemail + password→ bcrypt.verify()2. generate_tokenSecureRandom(96) → clear_tokenSHA-256(clear) → token_digest→ client (une seule fois)clear_token en JSON→ PostgreSQLtoken_digest + expires_at3. Requêtes suivantesAuthorization: Bearer xK7f…2mQ→ SHA-256(token)→ SELECT WHERE token_digest = ?Header HTTP standard→ comparaison digest→ touch(:last_accessed_at)4. Expiration automatiqueexpires_at = création + 3 moisscope :active filtre les tokens expirés5. Déconnexioninvalidate_tokens! → canceled_at = nowle token clair n’est jamais stocké

Le token clair (128 caractères) est renvoyé au client une seule et unique fois, à la création. Côté serveur, on ne stocke que le digest SHA-256. Si la base de données est compromise, les tokens sont inutilisables. Le concern TokenAuthenticatable est inclus dans les modèles User et Provider — polymorphisme classique Rails.

Le concern côté contrôleur intercepte chaque requête et charge l’utilisateur courant :

# app/controllers/concerns/token_authentication.rb
module TokenAuthentication
  extend ActiveSupport::Concern

  included do
    before_action :authenticate_with_token!
  end

  def current_user
    @current_user ||= lookup_user_from_token
  end

  private

  def lookup_user_from_token
    token = request.headers['Authorization']&.gsub(/^Bearer /, '')
    return nil unless token.present?
    AccessToken.find_by_access_token(token)&.authenticatable
  end
end

C’est volontairement simple. Pas de JWT (on n’a pas besoin de claims embarqués dans le token), pas de refresh token (l’expiration à trois mois suffit pour une app mobile), pas d’OAuth2 complet (surdimensionné pour un projet mono-client). Le token est un identifiant opaque — il identifie, il n’autorise pas.

Les concerns : notre arme secrète

Rails 4 avait introduit les concerns — des modules réutilisables inclus dans les modèles. Sur Hively, on a poussé ce pattern plus loin que sur aucun projet précédent. Chaque aspect transversal du domaine est encapsulé dans un concern :

# Le TokenAuthenticatable fonctionne pour User ET Provider
module TokenAuthenticatable
  extend ActiveSupport::Concern

  included do
    has_many :access_tokens, as: :authenticatable, dependent: :destroy
  end

  def generate_access_token!
    access_tokens.create!
  end

  def invalidate_tokens!
    access_tokens.active.update_all(canceled_at: Time.current)
  end
end

# Le SocialAuthenticatable gère Facebook pour les deux modèles
module SocialAuthenticatable
  extend ActiveSupport::Concern

  class_methods do
    def find_or_create_from_social_profile!(provider, profile)
      social = SocialId.find_by(provider: provider, social_id: profile[:id])
      return social.social_authenticatable if social

      ActiveRecord::Base.transaction do
        user = create!(
          email: profile[:email],
          first_name: profile[:first_name],
          last_name: profile[:last_name]
        )
        user.social_ids.create!(provider: provider, social_id: profile[:id])
        user
      end
    end
  end
end

Le résultat : User et Provider partagent exactement le même code d’authentification. Quand on a ajouté la connexion Facebook pour les prestataires (deux mois après la livraison initiale pour les utilisateurs), il a suffi d’ajouter include SocialAuthenticatable dans le modèle Provider. Zéro duplication.

On a huit concerns sur ce projet : TokenAuthenticatable, SocialAuthenticatable, Profilable, Postcodes, Password, Username, Referrable, UserFavorite. Chacun encapsule une responsabilité unique. Le modèle User fait 50 lignes — pas 500.

ApplicationRecord : le nouveau standard

Un détail de Rails 5 qui simplifie les concerns : ApplicationRecord. Avant, tous les modèles héritaient de ActiveRecord::Base directement — il n’y avait pas de classe intermédiaire pour mutualiser du comportement. Si on voulait ajouter une méthode à tous les modèles, on devait monkey-patcher ActiveRecord::Base (dangereux) ou créer un concern inclus partout (verbeux).

Rails 5 introduit ApplicationRecord — une classe abstraite qui s’intercale entre les modèles et ActiveRecord::Base. On y met les comportements communs à tous nos modèles : les conventions de sérialisation JSON, les scopes partagés, les callbacks de logging. C’est propre, explicite, et ça ne pollue pas le framework.

Le safe navigation operator &.

Ruby 2.3 (sorti en décembre dernier) nous a donné &. — l’opérateur de navigation sûre. On l’utilise partout dans les contrôleurs API :

# Avant — verbeux et défensif
def current_user
  token = AccessToken.find_by_access_token(raw_token)
  if token
    token.authenticatable
  else
    nil
  end
end

# Après — une ligne, idiomatique
def current_user
  AccessToken.find_by_access_token(raw_token)&.authenticatable
end

Ce n’est pas spécifique à Rails, mais Ruby 2.3 + Rails 5 forment un combo où chaque ligne de code est plus concise et plus sûre. Le &. élimine les nil checks défensifs qui polluaient nos contrôleurs — et chaque ligne gagnée est une ligne de moins à maintenir.

Le moteur de disponibilités

C’est le morceau le plus intéressant de l’architecture — et le plus complexe.

Le problème

Un utilisateur cherche un prestataire de ménage disponible mardi prochain entre 9h et 12h dans le quartier de Shoreditch. L’API doit :

  1. Trouver les prestataires qui couvrent ce code postal
  2. Qui proposent le service “ménage”
  3. Qui sont disponibles sur ce créneau — c’est-à-dire qui ont déclaré être disponibles le mardi, qui n’ont pas posé de congé ce jour-là, et qui n’ont pas déjà une réservation confirmée sur cette plage

Les deux premiers filtres sont triviaux (des WHERE sur la base). Le troisième est un calcul.

La composition de plages horaires

On a développé une classe RangeComposition qui manipule des ensembles de plages numériques. Le principe est simple : on part des disponibilités du prestataire, on soustrait les congés et les réservations, on obtient les créneaux libres.

class RangeComposition
  attr_accessor :ranges

  def initialize(exclude_border: false)
    @ranges = []
    @exclude_border = exclude_border
  end

  def add(range_or_ranges)
    Array(range_or_ranges).each { |r| add_range(r) }
    self
  end

  def remove(range_or_ranges)
    Array(range_or_ranges).each { |r| remove_range(r) }
    self
  end

  private

  def add_range(new_range)
    before, overlapping, after = @ranges.partition_three { |r| overlaps?(r, new_range) }
    merged_begin = [new_range.begin, *overlapping.map(&:begin)].min
    merged_end   = [new_range.end,   *overlapping.map(&:end)].max
    @ranges = before + [merged_begin..merged_end] + after
  end

  def remove_range(other)
    @ranges = @ranges.flat_map do |range|
      if overlaps?(range, other)
        remaining = []
        remaining << (range.begin..other.begin) if range.begin < other.begin
        remaining << (other.end..range.end)     if other.end < range.end
        remaining
      else
        [range]
      end
    end
  end

  def overlaps?(a, b)
    a.begin <= b.end && b.begin <= a.end
  end
end
RANGECOMPOSITION — CALCUL DES CRÉNEAUX DISPONIBLESOn part des disponibilités hebdomadaires, on soustrait congés et réservations08:0010:0012:0014:0016:00Disponibilités(mardi habituel)08:00 — 12:0013:00 — 16:00Réservation(confirmée)09:00 — 11:00remove(9h..11h)Après soustraction08–09h11–12h13:00 — 16:00Buffer 12h(si aujourd’hui)trop tardCréneaux libres11–12h13:00 — 16:00→ renvoyés à l'app mobile comme tableau de {beginning_at, ending_at}

L’algorithme de add_range est le point délicat. Quand on ajoute une plage, il faut fusionner toutes les plages existantes qui la chevauchent. On partitionne les plages en trois groupes : celles qui sont avant (pas de chevauchement), celles qui chevauchent, et celles qui sont après. Les plages qui chevauchent sont fusionnées en prenant le min des débuts et le max des fins.

remove_range fait l’inverse : pour chaque plage existante qui chevauche la plage à retirer, on coupe et on garde les morceaux restants.

L’assemblage dans le modèle Provider

Le modèle Provider utilise RangeComposition pour calculer ses créneaux disponibles :

class Provider < ApplicationRecord
  def slots(starting_day: 1, ending_day: 14)
    composition = RangeComposition.new(exclude_border: true)

    # 1. Ajouter les disponibilités hebdomadaires
    composition.add(available_ranges(
      starting_day: starting_day,
      ending_day: ending_day
    ))

    # 2. Soustraire les congés
    composition.remove(holidays_ranges)

    # 3. Soustraire les réservations confirmées
    composition.remove(booked_availability_ranges)

    # 4. Soustraire le passé (+ buffer de 12h)
    composition.remove([0..12.hours.from_now.to_i])

    # 5. Convertir en créneaux exploitables
    composition.ranges.map do |range|
      {
        beginning_at: Time.at(range.begin).utc,
        ending_at:    Time.at(range.end).utc
      }
    end
  end

  private

  def available_ranges(starting_day:, ending_day:)
    working_days(starting_day: starting_day, ending_day: ending_day)
      .flat_map do |date|
        availabilities.active.where(weekday: date.wday).map do |a|
          day_start = date.beginning_of_day.to_i
          (day_start + a.beginning)..(day_start + a.ending)
        end
      end
  end
end

Le exclude_border: true est subtil. Quand un prestataire a un créneau de 9h à 12h et un autre de 12h à 15h, on ne veut pas les fusionner en un seul créneau de 9h à 15h — 12h est une frontière intentionnelle (pause déjeuner, par exemple). L’option exclude_border considère que deux plages qui se touchent exactement ne se chevauchent pas.

La validation de créneau

Quand l’utilisateur réserve un créneau, on ne se contente pas de vérifier que le créneau est dans les disponibilités — on vérifie qu’il respecte les contraintes métier :

class AbstractBooking < ApplicationRecord
  validate :beginning_at_is_a_valid_multiple_of_30_minutes
  validate :ending_at_is_a_valid_multiple_of_15_minutes
  validate :datetime_in_slots

  private

  def beginning_at_is_a_valid_multiple_of_30_minutes
    return unless beginning_at
    unless beginning_at.min % 30 == 0
      errors.add(:beginning_at, "must start on a 30-minute slot")
    end
  end

  def datetime_in_slots
    return unless provider && beginning_at
    unless provider.datetime_in_slots?(beginning_at, booking_id: id)
      errors.add(:beginning_at, "is not in provider's available slots")
    end
  end
end

Les créneaux commencent toutes les 30 minutes (9:00, 9:30, 10:00…) — c’est un compromis entre granularité et complexité d’interface. La validation datetime_in_slots rappelle le calcul de disponibilité du provider en excluant la réservation courante (pour permettre la modification).

La machine à états des réservations

AASM pour les transitions

Une réservation n’est pas un simple enregistrement en base — c’est un objet métier avec un cycle de vie. On utilise AASM (Acts As State Machine) pour modéliser les transitions et les effets de bord :

MACHINE À ÉTATS — CYCLE DE VIE D’UNE RÉSERVATIONChaque transition déclenche des effets de bord — emails, SMS, jobs SidekiqpendingcreateSMS prestataire + email utilisateurconfirmedconfirm!email + ReminderBookingJobdeclineddecline!cancelledcancel!cancel!CancelBookingJobcompletedcomplete!CancelBookingJobLastChanceConfirmJobannule après délai sans réponserelance le prestataire avant annulationSidekiqFeedServiceRedis-backed, avec retry automatiquemessages dans la conversation liée
class AbstractBooking < ApplicationRecord
  include AASM

  aasm column: :state do
    state :pending, initial: true
    state :confirmed, :declined, :cancelled, :completed

    event :confirm do
      transitions from: :pending, to: :confirmed
      after do
        FeedService.booking_request_accepted(self)
        ReminderBookingJob.set(wait_until: beginning_at - 2.hours)
                          .perform_later(id)
      end
    end

    event :decline do
      transitions from: :pending, to: :declined
      after { FeedService.booking_declined(self) }
    end

    event :cancel do
      transitions from: [:pending, :confirmed], to: :cancelled
      after { CancelBookingJob.perform_later(id) }
    end

    event :complete do
      transitions from: :confirmed, to: :completed
      after { FeedService.booking_completed(self) }
    end
  end
end

AASM apporte trois choses qu’un simple champ status ne donne pas :

  1. Les transitions sont déclaratives. On ne peut pas passer de declined à confirmed — AASM lève une exception. Pas besoin de if/else défensif dans les contrôleurs.
  2. Les callbacks sont liés aux transitions. L’email de confirmation part quand on passe de pending à confirmed, pas quand le champ state contient confirmed. La nuance est importante : un état restauré manuellement en base ne déclenche pas les effets de bord.
  3. Les scopes sont générés automatiquement. Booking.confirmed, Booking.pending — pas besoin de les écrire.

Les réservations récurrentes

Un cas métier qui a demandé du travail : les plans de réservation. Un utilisateur peut réserver un ménage hebdomadaire — tous les mardis de 9h à 12h. Le BookingPlan génère les Booking enfants :

class BookingPlan < AbstractBooking
  has_many :bookings

  def create_bookings(start_date = beginning_at)
    book_datetimes(start_date).each do |datetime|
      bookings.create!(
        user: user,
        provider: provider,
        service: service,
        beginning_at: datetime,
        ending_at: datetime + duration.minutes,
        duration: duration,
        address: address,
        options: options,
        state: state  # les enfants héritent de l'état du plan
      )
    end
  end
end

Le job BookingPlanJob est déclenché quand le plan est confirmé. Il génère les réservations individuelles pour les prochaines semaines, en vérifiant la disponibilité du prestataire pour chaque créneau. Si un créneau est déjà pris, il est sauté — pas de conflit silencieux.

Les profils polymorphiques

Le défi du multi-services

Un prestataire de ménage a un tarif horaire et des options (repassage, linge, matériel fourni). Un coach fitness propose du yoga, du pilates ou du personal training en sessions de 30 ou 60 minutes avec des tarifs différents. Un dog-walker distingue les balades solo et en groupe, par taille de chien.

On ne peut pas mettre tout ça dans un seul modèle Provider. La solution : des profils polymorphiques — un modèle par vertical, liés au provider par une association belongs_to :profile, polymorphic: true.

PROFILS POLYMORPHIQUES — UN MODÈLE PAR VERTICALELe Provider délègue les attributs métier à un profil typé — le concern Profilable mutualise la logique communeProviderprofile_type: “Profile::Cleaning”profile_id: 42belongs_to :profile, polymorphic: trueProfile::Cleaninghourly_rate_centsbrings_equipment_rateironing: booleanlaundry: booleanworks_with_pets: booleandbs_checked: booleanProfile::Fitnessyoga_30_rate_centspilates_60_rate_centspt_30_rate_centsyoga: booleanpilates: booleanpersonal_training: booleanProfile::DogWalkingsolo_30_rate_centsgroup_30_rate_centssmall_dogs: booleanmedium_dogs: booleanlarge_dogs: booleanmax_dogs: integerinclude Profilableinclude Profilableinclude Profilable

Le concern Profilable et la configuration YAML

La vraie astuce, c’est le concern Profilable combiné avec un fichier profiles.yml. Plutôt que de coder en dur les attributs de chaque profil, on les déclare dans un YAML :

# config/profiles.yml
cleaning:
  attributes:
    hourly_rate:
      type: MinMax
      min: 8
      max: 50
      step: 1
    ironing:
      type: Boolean
    laundry:
      type: Boolean
    brings_equipment:
      type: Boolean
  booking_options:
    - ironing
    - extra_clean

dog_walking:
  attributes:
    solo_30_rate:
      type: MinMax
      min: 5
      max: 30
    group_30_rate:
      type: MinMax
      min: 3
      max: 20
    large_dogs:
      type: Boolean
    max_dogs:
      type: Direct
  booking_options:
    - walk_type
    - dog_size

Le concern Profilable charge cette configuration au boot et génère dynamiquement les validations, les scopes de filtrage et les attributs exposés par l’API :

module Profilable
  extend ActiveSupport::Concern

  included do
    has_one :provider, as: :profile

    def self.profile_config
      Rails.application.config_for(:profiles)[service_key]
    end

    def self.filtrable_attributes
      profile_config['attributes'].select { |_, v| v['filtrable'] }
    end

    def self.displayable_attributes
      profile_config['attributes'].select { |_, v| v['displayable'] }
    end
  end

  def short_description
    # Génère une description lisible à partir des attributs actifs
    active_attrs = self.class.displayable_attributes.keys
                       .select { |attr| send(attr) }
    active_attrs.map(&:humanize).join(', ')
  end
end

L’avantage est considérable : quand on a ajouté la verticale “beauté” en cours de projet, on a créé une migration pour la table profile_blow_dries, ajouté une entrée dans profiles.yml, et c’était en ligne. Pas de changement dans le contrôleur API, pas de changement dans le FilterService, pas de changement dans le back-office — tout est piloté par la configuration.

Le FilterService

Le FilterService lit la configuration YAML pour savoir quels filtres appliquer à chaque service. Un filtre MinMax sur le tarif devient un WHERE hourly_rate_cents BETWEEN ? AND ?. Un filtre Boolean sur “repassage” devient un WHERE ironing = true. Le service encapsule cette logique :

class FilterService
  def initialize(providers, params, service)
    @providers = providers
    @params = params
    @config = service.profile_class.constantize.profile_config
  end

  def filter
    @config['attributes'].each do |attr_name, attr_config|
      value = @params[attr_name]
      next unless value.present?

      @providers = case attr_config['type']
      when 'Boolean'
        @providers.where(attr_name => cast_boolean(value))
      when 'MinMax'
        @providers.where("#{attr_name}_cents >= ? AND #{attr_name}_cents <= ?",
          value['min'].to_i * 100, value['max'].to_i * 100)
      when 'ChoiceSet'
        @providers.where("#{attr_name} = ANY(?)", value)
      end
    end
    @providers
  end
end

C’est du Rails idiomatique — on compose des scopes ActiveRecord. La requête SQL finale est construite par enchaînement de where, et ActiveRecord s’occupe de l’optimisation. Sur un dataset de quelques centaines de prestataires par zone géographique, c’est instantané.

Sidekiq : les jobs en arrière-plan

Le projet utilise cinq jobs Sidekiq, chacun avec une responsabilité claire :

# Génère les réservations d'un plan récurrent
class BookingPlanJob < ApplicationJob
  def perform(booking_plan_id)
    plan = BookingPlan.find(booking_plan_id)
    plan.create_bookings
  end
end

# Annule les réservations pending après délai
class CancelBookingJob < ApplicationJob
  def perform(booking_id)
    booking = AbstractBooking.find(booking_id)
    return unless booking.pending?
    booking.cancel!
  end
end

# Rappel 2h avant la prestation
class ReminderBookingJob < ApplicationJob
  def perform(booking_id)
    booking = AbstractBooking.find(booking_id)
    return unless booking.confirmed?
    FeedService.reminder_booking_upcoming(booking)
  end
end

Le pattern est toujours le même : le job reçoit un ID, recharge l’objet, vérifie l’état, agit. Le return unless booking.pending? est important — entre le moment où le job est planifié et le moment où il s’exécute, l’état peut avoir changé. Un job doit être idempotent et résistant aux changements d’état.

Sidekiq tourne sur Redis avec six workers en concurrence. Les mailers ont leur propre queue avec une priorité inférieure — un pic d’envoi d’emails ne bloque pas les jobs métier.

Ce qu’on en retient

Après Simone.paris, Hively confirme que Rails 5 est le bon choix pour les API mobiles — et qu’on avait raison de migrer dès la sortie. Le mode API officialise une pratique qu’on défend depuis deux ans. Action Cable intègre le temps réel sans dépendance externe. La commande unifiée rails élimine une source de confusion quotidienne.

Là où d’autres équipes hésitent encore entre Express.js, Sails ou Laravel pour leurs backends d’apps mobiles, on livre des projets complets en Rails avec un écosystème qui n’a pas d’équivalent : Devise pour l’auth, AASM pour les machines à états, Sidekiq pour les jobs asynchrones, ActiveAdmin pour le back-office, CarrierWave pour les uploads, Mandrill et Nexmo pour les notifications. Tout s’emboîte. On ne passe pas trois jours à choisir et câbler des briques — on les branche et on se concentre sur le métier.

Les patterns qu’on recommande :

  1. Tokens plutôt que sessions pour les APIs mobiles. Pas besoin de JWT — un token opaque avec digest SHA-256 couvre 95% des cas. Plus simple, plus sûr, plus facile à révoquer.
  2. AASM pour tout ce qui a un cycle de vie. Les réservations, les inscriptions de prestataires, les candidatures — dès qu’un objet passe par des états, AASM rend les transitions explicites et les effets de bord prévisibles. On l’utilise systématiquement sur tous nos projets depuis Simone.
  3. Le polymorphisme Rails, avec modération. Les profils polymorphiques nous ont sauvé la mise sur un projet multi-services. Mais on ne polymorphise que ce qui est vraiment hétérogène — les adresses, les tokens, les participants de conversation sont polymorphiques aussi, et ça ajoute de la complexité au schéma.
  4. La configuration YAML plutôt que le code pour les attributs métier qui varient par verticale. C’est l’approche la plus ambitieuse du projet — et la plus payante. Ajouter un service sans toucher au code applicatif, c’est la différence entre une demi-journée et une semaine de développement.

Rails 5.1 est déjà annoncé avec l’intégration native de Webpack via Webpacker et le support de Yarn. La direction est claire : Rails embrasse l’écosystème JavaScript frontend au lieu de le combattre. C’est exactement ce qu’on fait déjà — on a juste un peu d’avance.

Le prochain article portera sur le front Ionic — le pipeline Webpack et ES6 qu’on a mis en place.