Skip to content
Fancy Pixel Fancy Pixel
Torna al blog

React + Flux con backend Rails API - Parte 1

Costruire un'app moderna spezzata in due: un'API Rails minimale e un frontend React con architettura Flux. Parte 1: setup di rails-api, token authentication, CORS.

Andrea Mazzini 8 min di lettura

Versione italiana dell’articolo originale del 28 gennaio 2015; traduzione a cura del team.

Sto lavorando al frontend di un progetto che stiamo sviluppando qui in Fancy Pixel. Stiamo prendendo quella che sembra una buona abitudine: tagliare quella che sarebbe un’app monolitica Rails in un backend leggero che serve API e un frontend che le consuma. Lo abbiamo già fatto in un passato non troppo lontano con Angular.js. Andava tutto bene, finché non è andato più bene. C’è qualcosa che non mi convince in Angular, non entro nei dettagli (l’hanno già fatto in molti), ma diciamo che c’è troppa magia coinvolta per i miei gusti (dice quello che usa Rails). La magia mi sta bene finché posso capire come smanettare con gli interni quando le cose vanno a sud. Con Angular lo sforzo sembra troppo: ma è solo gusto personale. Non posso poi negare che i grossi cambiamenti strutturali introdotti con la 2.0 siano stati l’ultimo chiodo nella bara. Volevo provare qualcosa di nuovo, qualcosa che imponesse un’architettura solida alle nostre app, lasciandomi controllare i singoli ingranaggi dell’engine. React ha ricevuto molta buona stampa negli ultimi mesi, quindi ho colto l’occasione per buttarmi. In questo post in tre parti troverai più o meno tutto quello che ho imparato scrivendo un frontend con React, architettura Flux vanilla, che consuma un’API scritta in Rails.

Scegliere il backend

Vista la nostra esperienza, la scelta ovvia per noi era Rails, ma con una variante: rails-api. Rails-api è una versione “spogliata” di Rails, dove gran parte del middleware “inutile” non è inclusa (ma puoi includerlo se ti serve). Usare Rails per servire JSON può sembrare overkill ai più, ma la pagina GitHub di rails-api ha degli ottimi argomenti per controbattere, e li trovo azzeccati. TL;DR: Rails è meraviglioso, sfruttiamolo.

La tecnologia per il frontend

React è una libreria JavaScript per costruire user interface, sviluppata e open source dagli ingegneri di Facebook. Il suo principale punto di forza è la capacità di fornire un modo dinamico e veloce per costruire app isomorfiche. “Isomorfico” vuol dire che l’app può essere renderizzata con facilità sia sul server che sul client, cosa utile per la SEO. Personalmente della SEO non potrebbe importarmene di meno, anche se ne capisco l’importanza… ciò che mi ha convinto di React è il Virtual DOM e il modo in cui i dati sono organizzati e gestiti nelle view. Il Virtual DOM è qualcosa che vedremo implementato anche in altri framework JS (Ember lo fa già, se non sbaglio). Le view possono essere renderizzate sul server al primo request, poi la tecnologia sottostante renderizza le pagine successive in un Virtual DOM, che viene poi diffato con il DOM reale: solo le differenze diventano cambiamenti nella pagina visibile. Ed è veloce. Brillante. Questo ci consente di scrivere frontend come si faceva una volta: in modo dichiarativo. Specifichiamo come dovrebbe apparire la UI, e quando i dati cambiano React si occupa del refresh della pagina, cambiando solo le parti che devono essere cambiate.

Flux

Questo copre backend e view, ci manca qualcosa nel mezzo: un’architettura da seguire. Flux è un’architettura per costruire web UI e funziona molto bene insieme a React (ma può davvero essere applicata ovunque).

Iniziano i guai

Non sono mai stato un grande fan dell’implementare web UI: il CSS diventa sempre un casino, i file JavaScript diventano monoliti spaventosi dove il codice di bassa qualità va a morire, mentre gli sviluppatori testano le proprie doti speleologiche e perdono la sanità mentale. Forse sono solo io che faccio schifo, ma neppure usare Sass e CoffeeScript ha mai risolto davvero i miei problemi. Ero entusiasta di provare qualcosa di davvero nuovo, ma sapevo che iniziare con una tecnologia così giovane sarebbe stata una rogna importante. Caso esemplare: imparare e iniziare a essere produttivo (cioè: scrivere codice utilizzabile) ha richiesto un bel po’ di grattate di testa. Non c’è ancora una “best practice” chiara per fare i task comuni, né una configurazione iniziale chiara. Diciamola così: se vieni dal mondo RoR, dove la convention over configuration riduce di parecchio il boilerplate e ti “forza” a seguire le best practice consolidate, con Flux farai fatica. Questo post copre le soluzioni a cui siamo arrivati: non saranno perfette, ma sono ragionevolmente sicuro che non ci siano anti-pattern dentro, e sono un buon punto di partenza.

Mettere tutto insieme

Iniziamo a scrivere un po’ di codice. Faremo una semplice app Rails con signup, login e la possibilità di postare una story. Come una versione mignon di Medium. Chiamiamola appropriatamente Small.

Se ti interessano solo Flux e React, salta pure la parte Rails e vai dritto alla Parte 2.

Rails API

Un po’ di tempo fa mi sono imbattuto in questo articolo di Alan Peabody. Ho avuto un’esperienza simile alla sua: inizi a lavorare su un progetto, usi gli strumenti giusti, fai del tuo meglio per imporre buoni pattern, ma alla fine il codice frontend diventa spaventoso, qualcosa che nessuno vuole più mantenere. Rompiamo con l’asset pipeline, come dice il titolo, e lavoriamo per rendere Rails di nuovo bello. Useremo la gem rails-api per questo task. Puoi generare una nuova app col suo comando CLI, o puoi integrarla dopo. Io scelgo la seconda opzione, senza una vera ragione: pura abitudine.

rails new small

Aggiungiamo poi le gem rails-api, devise, active_model_serializers al Gemfile, e già che ci siamo togliamo tutte le gem che generano asset o contenuto delle view, jbuilder incluso. Il nostro Gemfile dovrebbe sembrare così (sezione di test omessa):

source 'https://rubygems.org'

gem 'rails', '4.2.0'
gem 'rails-api', '~> 0.4.0'
gem 'active_model_serializers', '~> 0.8.3' # NOTE: not the 0.9
gem 'devise', '~> 3.4.1'
gem 'sqlite3'
gem 'sdoc', '~> 0.4.0', group: :doc
gem 'thin'

group :development, :test do
  gem 'faker'
  gem 'byebug'
  gem 'web-console', '~> 2.0'
  gem 'spring'
end

Ora dobbiamo modificare l’application controller perché erediti da ActionController::API, e dire addio a protect_from_forgery. Dato che serviamo solo JSON, ha senso aggiungere

respond_to :json

all’application controller: aiuta a DRY-are tutto. Già che ci siamo, possiamo anche eliminare le cartelle assets e views, non ci serviranno.

Autenticazione

Devo prima definire una risorsa? Forse, ma è banale, togliamoci di mezzo l’autenticazione. Stiamo costruendo un’API, quindi nessuna session sarà coinvolta: dobbiamo autenticare l’utente in ogni request. Userò l’Oauth2 Resource Owner Password Credentials Grant, che suona fighissimo ma in pratica è solo un token nell’header della request che autentica il caller. La gem Devise implementava una strategia token_authenticatable, ma è stata rimossa per motivi di sicurezza. Ci sono gem che implementano la strategia (come Doorkeeper), ma dato che è piuttosto facile da implementare lo faccio da solo. Prima installiamo Devise aggiungendolo al Gemfile e lanciando rails generate devise:install dopo un bundle install, poi creiamo il model User:

rails generate devise User

Token authentication

La token authentication è stata rimossa da Devise un paio d’anni fa, questo link spiega perché. Dobbiamo implementarla noi, ma è abbastanza facile. Il token sarà composto da due informazioni: l’id dell’utente seguito dal token vero e proprio, separati da :. Per questo sample useremo l’id del database dell’utente, per semplicità — ma ovviamente non è una cosa intelligente da fare. Per cominciare aggiungiamo un access_token all’utente (e già che ci siamo, anche uno username):

class AddAccessTokenToUser < ActiveRecord::Migration
  def change
    add_column :users, :access_token, :string
    add_column :users, :username, :string
  end
end

ed ecco il model User:

# app/models/user.rb
class User < ActiveRecord::Base
  devise :database_authenticatable, :recoverable, :validatable

  after_create :update_access_token!

  validates :username, presence: true
  validates :email, presence: true

  private

  def update_access_token!
    self.access_token = "#{self.id}:#{Devise.friendly_token}"
    save
  end

end

L’autenticazione dell’utente vivrà nell’application controller:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include AbstractController::Translation

  before_action :authenticate_user_from_token!

  respond_to :json

  ##
  # User Authentication
  # Authenticates the user with OAuth2 Resource Owner Password Credentials Grant
  def authenticate_user_from_token!
    auth_token = request.headers['Authorization']

    if auth_token
      authenticate_with_auth_token auth_token
    else
      authentication_error
    end
  end

  private

  def authenticate_with_auth_token auth_token
    unless auth_token.include?(':')
      authentication_error
      return
    end

    user_id = auth_token.split(':').first
    user = User.where(id: user_id).first

    if user && Devise.secure_compare(user.access_token, auth_token)
      # User can access
      sign_in user, store: false
    else
      authentication_error
    end
  end

  ##
  # Authentication Failure
  # Renders a 401 error
  def authentication_error
    # User's token is either invalid or not in the right format
    render json: {error: t('unauthorized')}, status: 401  # Authentication timeout
  end
end

Concludiamo il processo di auth fornendo le route e il session controller:

# config/routes.rb
Rails.application.routes.draw do
  devise_for :user, only: []

  namespace :v1, defaults: { format: :json } do
    resource :login, only: [:create], controller: :sessions
  end
end

e il session controller:

# app/controllers/v1/sessions_controller.rb
module V1
  class SessionsController < ApplicationController
    skip_before_action :authenticate_user_from_token!

    # POST /v1/login
    def create
      @user = User.find_for_database_authentication(email: params[:username])
      return invalid_login_attempt unless @user

      if @user.valid_password?(params[:password])
        sign_in :user, @user
        render json: @user, serializer: SessionSerializer, root: nil
      else
        invalid_login_attempt
      end
    end

    private

    def invalid_login_attempt
      warden.custom_failure!
      render json: {error: t('sessions_controller.invalid_login_attempt')}, status: :unprocessable_entity
    end

  end
end

Il SessionSerializer è un oggetto Active Model Serializer, qualcosa tipo:

# app/serializers/v1/session_serializer.rb
module V1
  class SessionSerializer < ActiveModel::Serializer

    attributes :email, :token_type, :user_id, :access_token

    def user_id
      object.id
    end

    def token_type
      'Bearer'
    end

  end
end

E voilà. Migra, lancia il server e crea un utente via console. Dovresti ottenere qualcosa del genere:

$ curl localhost:3000/v1/login --ipv4 --data "username=user@example.com&password=password"
{
  "token_type": "Bearer",
  "user_id": 1,
  "access_token": "1:MPSMSopcQQWr-LnVUySs"
}

Creare una risorsa

Non entro nei dettagli qui, il task è puro RoR. Creiamo una risorsa Story e un controller che gestisce la creazione. Trovi l’app Rails completa in questo repo. Andiamo avanti.

CORS

Vale la pena notare che la UI ora non sarà servita da Rails: potrebbe vivere addirittura su un server diverso. Esistono soluzioni per tenere UI e API sullo stesso dominio, ma per ora ci serve abilitare il Cross-origin resource sharing (CORS). Lo facciamo aggiungendo la gem rack-cors al Gemfile e poi mettendo questo in config/application.rb:

config.middleware.insert_before 'Rack::Runtime', 'Rack::Cors' do
  allow do
    origins '*'
    resource '*',
             headers: :any,
             methods: [:get, :put, :post, :patch, :delete, :options]
  end
end

Questo apre verso qualsiasi dominio, quindi maneggia con cura.

Prossima puntata

Fine per la parte Rails. È una gioia scrivere solo l’API in Rails. È più facile da testare, più facile da mantenere, ed è velocissimo. Ora procediamo col frontend. Per leggibilità spezzo l’articolo qui, salta alla Parte 2 per iniziare a costruire il frontend.

Parte 2