# Simone.paris — pourquoi Rails pour le backend d'une app iOS > Retour d'expérience sur le backend Rails de l'app Simone.paris : API Grape pour iOS, machine à états AASM, Service Objects, et un dashboard ActiveAdmin complet livré à la cliente. Date : 18/05/2015 Auteur : Raphael P. Tags : Rails, Ruby, API, ActiveAdmin, Architecture --- La semaine dernière, Emmanuelle et Aurélien ont publié [leur retour sur l'app iOS de Simone.paris](/blog/simone-paris-ios-retour-experience). Côté backend, c'est un projet Rails classique — mais qui illustre bien pourquoi ce framework reste un choix redoutable pour servir une app mobile. L'app Simone permet de réserver une coiffure à domicile à Paris. Le parcours client est linéaire : choisir un service, un créneau, renseigner une adresse, payer. Derrière, le backend gère la logique métier — recherche de disponibilités, machine à états pour les réservations, paiement Adyen, notifications push, SMS, facturation PDF. Et un dashboard ActiveAdmin pour que la fondatrice puisse suivre son activité au quotidien. ## Pourquoi Rails On est fin 2014. Le choix du backend pour une app mobile, c'est souvent Rails, Django, Express ou du PHP. On part sur Rails pour des raisons pragmatiques : - **Convention over configuration** — la structure est imposée, chaque fichier a sa place. On ne perd pas de temps à débattre de l'architecture de dossiers. - **ActiveRecord** — le mapping objet-relationnel est prêt. Relations, validations, scopes, callbacks — tout est déclaratif. - **L'écosystème de gems** — pour chaque besoin transversal (auth, paiement, state machine, admin), il existe une gem éprouvée. On assemble plus qu'on ne code. - **ActiveAdmin** — un back-office complet généré depuis les modèles. C'est souvent ce qui fait la différence pour un client non-technique. Le `Gemfile` résume bien l'approche — chaque gem couvre un besoin précis : ```ruby gem 'rails', '~> 4.2.1' gem 'pg' # PostgreSQL gem 'devise', '3.4.0' # Authentification gem 'grape' # API REST gem 'grape-entity' # Sérialisation JSON gem 'aasm' # Machine à états gem 'money-rails', '~>1' # Gestion des prix gem 'sidekiq' # Jobs asynchrones gem 'rpush' # Notifications push iOS gem 'activeadmin', '~> 1.2.0' # Dashboard admin gem 'chartkick' # Graphiques gem 'audited-activerecord' # Audit trail gem 'paperclip' # Upload S3 (factures PDF) ``` ## L'API : Grape plutôt que Rails controllers Pour servir l'app iOS, on utilise Grape plutôt que les controllers Rails classiques. Grape est un micro-framework dédié aux API REST — il impose une structure déclarative qui convient mieux à une API pure qu'un controller Rails avec `respond_to :json`. Chaque endpoint est déclaratif — paramètres typés, codes HTTP documentés, sérialisation par Entity : ```ruby module Api class Reservations < Grape::API resource :reservations do desc 'Create a new reservation', entity: Api::Entities::Reservation before params do requires :user_id, type: String requires :treatment_id, type: String optional :from, type: DateTime end post '/' do reservation = Reservation.create(parameters.to_h.merge(source: simone_source)) present reservation, with: Api::Entities::Reservation end end end end ``` Le `present` délègue la sérialisation à un Entity. C'est le même principe que le Decorator côté iOS — le controller ne sait pas comment le JSON est construit : ```ruby class Reservation < Grape::Entity expose :id, :state, :user_id expose :treatment, using: Api::Entities::Treatment expose :contact, using: Api::Entities::Contact, if: -> (r, _) expose :suggested_contact, using: Api::Entities::Contact, unless: -> (r, _) do |r| r.user.last_delivery_address end end ``` Les lambdas dans `if:` / `unless:` permettent de conditionner l'exposition — ici, on renvoie soit le contact de la réservation, soit la dernière adresse connue de l'utilisateur. L'app iOS n'a pas à gérer cette logique. ## La machine à états : AASM C'est le coeur du backend. Une réservation passe par 13 états, de `waiting_for_client` à `paid`, avec des chemins alternatifs (annulation, absence du client, prestataire introuvable). Exprimer ça en `if/else`, c'est ingérable. AASM transforme le workflow en déclaration : Le code correspondant est déclaratif — on lit le workflow comme une spécification : ```ruby aasm column: :state do state :waiting_for_client, initial: true state :broadcasted state :accepted_by_contractor state :treatment_done state :payment_requested state :paid, final: true event :validate_by_client, after: %i(use_promotions copy_contact_on_user send_client_previous_confirmation schedule_sms_to_contractor) do transitions from: :waiting_for_client, to: :broadcasted, guard: :contact_id? end event :perform_treatment, after: %i(request_payment! execute!) do transitions from: :accepted_by_contractor, to: :treatment_done, guard: :reservation_can_be_completed? end event :pay, before: :referral_promotions, after: %i(create_invoice send_invoice) do transitions from: :payment_requested, to: :paid end end ``` Chaque transition a ses gardes (`guard:`) et ses callbacks (`after:`, `before:`). Quand le client valide sa réservation, cinq actions s'enchaînent automatiquement : application des promotions, copie du contact, email de pré-confirmation, SMS au prestataire, planification du timeout. On n'a rien à orchestrer manuellement. ## Les Service Objects Pour la logique métier complexe, on extrait des Service Objects. Le plus représentatif est `AvailabilitiesFinder` — trouver les créneaux disponibles pour un soin donné : ```ruby class AvailabilitiesFinder SCHEDULE_STEPS = 30.minutes BETWEEN_RESERVATION_TIME = 45.minutes def initialize(treatment: nil, postcode: nil) @treatment = treatment @postcode = postcode end def find range_to_include = contractors.flat_map @ranges = StepFunction.build( to_include: range_to_include, to_exclude: time_ranges_to_exclude ).to_time_ranges @result = @ranges.inject(Set[]) do |set, tr| set + tr.split_in_time_slots(treatment_duration, SCHEDULE_STEPS) end sort_result! end end ``` Le service agrège les disponibilités de tous les prestataires, exclut les réservations existantes (avec un tampon de 45 minutes pour le déplacement), retire les jours fériés et les vacances, puis découpe en créneaux de 30 minutes. Le tout repose sur `StepFunction`, un objet mathématique maison qui fusionne des intervalles temporels. Le controller ne voit qu'un appel : ```ruby service = AvailabilitiesFinder.new(treatment: treatment, postcode: postcode) service.find present service.result ``` ## ActiveAdmin : le dashboard pour la cliente C'est peut-être la partie la plus sous-estimée du projet. La fondatrice de Simone avait besoin de suivre son activité — nombre de réservations, chiffre d'affaires, répartition par arrondissement, par tranche horaire, par service. Sans budget pour un outil BI. ActiveAdmin a tout débloqué. En une journée, on a monté un dashboard KPI complet : ```ruby ActiveAdmin.register_page 'Dashboard' do content title: 'Tableau de bord' do panel 'Données Globales' do display_p_with_strong("Nombre d'inscrits :", users.count) display_p_with_strong('Nombre de clients :', users.with_reservations.count) end panel 'Prestations exécutées' do sum = Money.new(reservations.sum(:discounted_price_cents)) display_p_with_strong('CA facturé :', humanized_money_with_symbol(sum)) display_p_with_strong('Panier moyen :', humanized_money_with_symbol(sum / reservations.count)) end end end ``` Avec `chartkick` et `groupdate`, les graphiques se construisent en une ligne : ```ruby price_by_day = reservations.group_by_day(:created_at).sum(:discounted_price_cents) price_by_day = price_by_day.map line_chart [] ``` Le dashboard affiche le CA par jour, le nombre de prestations par arrondissement, la répartition par tranche horaire — le tout avec un filtre de dates. La cliente peut isoler un mois, un trimestre, comparer les tendances. ### La gestion des réservations ActiveAdmin génère le CRUD automatiquement depuis le modèle. On a customisé l'index pour afficher 17 colonnes (client, prestataire, état, prix, promotions...) avec des filtres Ransack : ```ruby ActiveAdmin.register Reservation do scope :priority, default: true scope "Accepted", :accepted_by_contractor filter :state, as: :select, collection: proc { Reservation.other_states.map { |c| [t("reservations.states.#"), c] } } filter :by_contact_postcode_in, label: "Code Postal", as: :string filter :by_promotion_in, label: 'Code Promotion', as: :string index do column :user do |r| link_to r.user.email, admin_user_path(r.user) end column :state do |r| display_order_state(r) end column :discounted_price do |r| humanized_money_with_symbol(r.discounted_price) end actions end end ``` Le scope `:priority` filtre par défaut les réservations qui nécessitent une action — broadcasted sans prestataire, ignorées par les prestataires. La fondatrice arrive le matin, elle voit immédiatement ce qui demande son attention. Les transitions d'état sont aussi exposées dans l'admin : ```ruby action_item(:event, only: :show) do resource.aasm.events.map do |e| link_to t("reservations.events.#"), make_event_admin_reservation_path(event: e.name) end end ``` Un bouton par transition possible. L'admin clique sur "Accepter par prestataire", la machine à états fait le reste — envoi des emails, SMS, mise à jour du statut. Le code métier est le même que celui de l'API. ## Le modèle Reservation : tout est déclaratif Le modèle `Reservation` est le fichier le plus dense du projet, mais il reste lisible grâce aux conventions Rails : ```ruby class Reservation < ActiveRecord::Base include ReservationStates # machine à états (concern) include Contactable # association polymorphe contact include DiscountableReservation # calcul de prix avec promotions belongs_to :user belongs_to :treatment belongs_to :contractor has_many :payments, dependent: :destroy has_many :user_promotions, -> monetize :discounted_price_cents monetize :buying_price_cents, allow_nil: true validates :contact, presence: true, if: :state_with_information_completed? scope :executed, -> scope :from_date_between, -> (from, to) scope :with_promotion, -> delegate :name, to: :service, prefix: true, allow_nil: true audited end ``` Vingt lignes qui décrivent les associations, les validations conditionnelles, les scopes pour les requêtes courantes, la délégation pour éviter la loi de Demeter, et l'audit automatique. C'est la force de Rails — on *déclare* le comportement, on ne l'implémente pas. ## Ce qu'on en retient Rails nous a permis de livrer un backend complet en quelques semaines — API, machine à états, paiement, notifications, dashboard admin. Les points-clés : - **Grape pour l'API** est un meilleur choix que les controllers Rails quand on sert uniquement une app mobile. La déclaration des paramètres, le versioning, les Entities pour la sérialisation — tout est plus explicite. - **AASM transforme un workflow complexe en code lisible**. Les 13 états et leurs transitions sont documentés par le code lui-même. Ajouter un état, c'est ajouter trois lignes. - **Les Service Objects** (`AvailabilitiesFinder`, `AddPromotionService`) gardent les modèles propres. La logique métier complexe a sa place — ni dans le controller, ni dans le modèle. - **ActiveAdmin + chartkick = un dashboard en une journée**. Pour une startup early-stage, c'est souvent la fonctionnalité la plus valorisée par le client. Pas besoin de Metabase, pas besoin de Tableau. - **Les conventions Rails libèrent la bande passante mentale**. On ne débat pas de l'arborescence, du nommage des fichiers, de l'endroit où mettre les validations. Tout est décidé d'avance. On code. L'app Simone a tourné en production sans accroc. Le backend a absorbé la charge, le dashboard a servi au quotidien, et la base de code est restée lisible un an plus tard quand on a dû faire des ajustements. C'est ça, le vrai bénéfice de Rails : pas la productivité du premier jour, mais la maintenabilité du jour 365.