Skip to content
Fancy Pixel Fancy Pixel
Torna al blog

API in Rails:<br/>render :json => the simple way

Una soluzione minimale per generare JSON in un'API Rails, senza Jbuilder né ActiveModel::Serializers: solo moduli Ruby con metodi che restituiscono Hash. Massima flessibilità, performance bare metal.

Alessandro Verlato 6 min di lettura

Versione italiana dell’articolo originale del 1° giugno 2015; traduzione a cura del team.

Ultimamente il mio lavoro a Fancy Pixel si è concentrato sul backend di un prodotto che stiamo per lanciare e per cui abbiamo deciso di costruire un server JSON API-only. Queste API possono essere consumate da client/service di terze parti ma sono anche usate dal nostro frontend. In questo breve report vorrei condividere con te la semplice soluzione che stiamo usando per la generazione del JSON e che, secondo me, può essere un’alternativa rapida e indolore ai sistemi più comuni come Jbuilder o ActiveModel::Serializers.

Rails, uno dei miei work buddy preferiti, mi è capitato di usarlo diverse volte su progetti che includevano lo sviluppo di API. Per curiosità personale, negli anni ho avuto l’occasione di provare diverse soluzioni per la generazione del JSON e devo dire che a volte ho trovato qualche difficoltà con certe tecnologie: ottime nella maggior parte delle funzionalità, in qualche caso ti costringono a percorsi strani per ottenere il risultato desiderato. A dirla tutta, il motivo principale di questa sperimentazione continua è probabilmente la ricerca costante del miglior bilanciamento tra comfort/facilità d’uso/velocità di sviluppo e performance: dopo tutti questi test sono arrivato alla soluzione attuale che, pur non essendo nulla di nuovo, secondo me può dare a qualcuno un’idea alternativa che combina grande flessibilità e performance bare metal.

Per farla breve…

Non voglio dilungarmi con racconti epici o “disquisizioni sul sesso degli angeli”, quindi ho preparato una banale demo app che trovi sul nostro account GitHub, per mostrarti di cosa sto parlando.

Tiriamo su rapidamente l’applicazione:

git clone "https://github.com/FancyPixel/serializers_demo"
cd serializers_demo
bundle
rake db:create && rake db:migrate && rake db:seed

Per gli scopi di questo articolo ho deciso di usare rails-api (se non lo conosci ti consiglio di provarlo) al posto di Rails standard, semplicemente perché è quello che stiamo usando ora nel progetto che ho menzionato all’inizio. Ovviamente gli stessi concetti si applicano identicamente a Rails vanilla. Apriamo insieme il codice e diamogli un’occhiata rapida: come vedi queste poche righe di codice non fanno altro che rispondere a tre route. Se guardi config/routes.rb troverai qualcosa tipo:

# config/routes.rb

Rails.application.routes.draw do
  namespace :v1, defaults: { format: :json } do
    get :jbuilder, to: 'comparison#jbuilder'
    get :ams, to: 'comparison#ams'
    get :simple, to: 'comparison#simple'
  end
end

Come vedi ho impostato json come formato di default e ho definito un namespace così da replicare un tipico processo di versioning delle API. Saltiamo all’unico controller esistente (comparison_controller) dove troviamo l’implementazione delle action chiamate dalle route. Ognuna di queste action fa esattamente la stessa cosa: carica un po’ di record dal DB e li renderizza come JSON, ma ciascuna lo fa a modo suo, usando rispettivamente jbuilder, ActiveModel::Serializers e “la mia soluzione” che chiamerò “simple”… che nome eh?

Non ci concentreremo sui primi due sistemi, perché con ogni probabilità tu padroneggi quelle tecnologie meglio di me, e poi non c’è nulla fuori standard nelle mie implementazioni: saltiamo invece a piè pari sull’action simple. Come le competitor non fa altro che renderizzare un po’ di JSON, ma stavolta viene chiamato l’helper method serialize_awesome_stuffs. Questo metodo è definito nel modulo V1::SimpleAwesomeStuffSerializer che il controller include. Trovi il modulo sotto app/serializers/v1: se apri il file noterai che è solo un plain modulo Ruby che definisce dei metodi.

# app/serializers/v1/simple_awesome_stuff_serializer.rb

module V1
  module SimpleAwesomeStuffSerializer

    def serialize_awesome_stuff(awesome_stuff = @awesome_stuff)
      {
          name: awesome_stuff.name,
          some_attribute: awesome_stuff.some_attribute,
          a_counter: awesome_stuff.a_counter
      }
    end

    def serialize_awesome_stuffs(awesome_stuffs = @awesome_stuffs)
      {
          awesome_stuffs: awesome_stuffs.map do |awesome|
            serialize_awesome_stuff awesome
          end
      }
    end
  end
end

Entrambi i metodi non fanno altro che restituire un Hash che definisce coppie chiave-valore che vogliamo restituire come JSON. In particolare serialize_awesome_stuffs crea un Array di Hash e internamente, giusto per fare un po’ di DRY, chiama serialize_awesome_stuff (singolare). Forse il naming complessivo non è il migliore al mondo, eh? Bonus: definire il parametro del metodo come awesome_stuffs = @awesome_stuffs rende il nostro codice più leggero e leggibile, perché se restiamo aderenti alle convenzioni di naming delle variabili, probabilmente il controller sta definendo qualcosa come @awesome_stuff (e in effetti è così) direttamente visibile e usabile dal modulo. Se ci viene un guizzo creativo e vogliamo usare nomi di variabili personali, non avremo nessun problema.

Prendi questo pezzo di codice come esempio:

# some_controller.rb

def some_action
  @my_personal_awesome_stuffs = AwesomeStuff.all
  render json: serialize_awesome_stuffs @my_personal_awesome_stuffs
end

e tutto funzionerà come previsto.

Step-by-step: complichiamo un po’ le cose

Alziamo un po’ la complessità del nostro esempio aggiungendo un model User e definendo una relazione one-to-many con il nostro AwesomeStuff:

rails g model user name:string

aggiungi la reference a User in AwesomeStuff:

rails g migration add_user_reference_to_awesome_stuff user:references

e migra tutto:

rake db:migrate

Ora definiamo le relazioni tra i model:

# app/models/awesome_stuff.rb
class AwesomeStuff < ActiveRecord::Base
  belongs_to :user
end

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :awesome_stuffs
end

lancia la console Rails con rails c e inserisci un po’ di dati di test nel DB:

# Add five users
users = []
5.times {|n| users << User.create(name: "user_#{n}") }

# Randomly associate our awesome records™ to users
AwesomeStuff.all.each { |aww| aww.update user: users.sample }

# A rapid test confirms that...
User.first.awesome_stuffs

=> #<ActiveRecord::Associations::CollectionProxy [#<AwesomeStuff id: ... ... >]>

Ora che è tutto pronto, seguiamo alcuni degli step che faremmo di solito durante la creazione di un’API. Creiamo uno UserController tramite cui restituire al client anche gli awesome record™ associati all’utente. Crea users_controller.rb sotto app/controllers/v1/ e aggiungi l’action index:

# app/controllers/v1/users_controller.rb

module V1
  class UsersController < ApplicationController
    include V1::UsersSerializer

    def index
      @users = User.all.includes(:awesome_stuffs)
      render json: serialize_users
    end
  end
end

Come vedi ho già aggiunto qualcosa che ci servirà a breve, ovvero il modulo V1::UsersSerializer. Nota anche lo scoping (V1) dei nostri serializer: in questo modo possiamo seguire l’evoluzione delle versioni della nostra API senza problemi, ridefinendo eventualmente solo il behavior dei serializer che cambiano. Non dimenticare di aggiungere le nuove route:

# config/routes.rb

Rails.application.routes.draw do
  namespace :v1, defaults: { format: :json } do
    get :jbuilder, to: 'comparison#jbuilder'
    get :ams, to: 'comparison#ams'
    get :simple, to: 'comparison#simple'

    # Let's add the 'index' only
    resources :users, only: :index
  end
end

Cosa aggiungeremo al nostro UsersSerializer? Una prima idea potrebbe essere qualcosa tipo:

# app/serializers/v1/users_serializer.rb

module V1
  module UsersSerializer

    def serialize_users(users = @users)
      {
        users: users.map do |user|
          {
              id: user.id,
              name: user.name,
              awesome_stuffs: user.awesome_stuffs.map do |aww|
                {
                    name: aww.name,
                    some_attribute: aww.some_attribute,
                    a_counter: aww.a_counter
                }
              end
          }
        end
      }
    end
  end
end

Ok, ma qui c’è un sacco di code smell, giusto? Abbiamo già visto un po’ di questa roba, proviamo a riusarla:

# app/serializers/v1/users_serializer.rb

module V1
  module UsersSerializer
    include V1::SimpleAwesomeStuffSerializer

    def serialize_users(users = @users)
      {
        users: users.map do |user|
          {
              id: user.id,
              name: user.name,
              awesome_stuffs: user.awesome_stuffs.map do |aww|
                serialize_awesome_stuff aww
              end
          }
        end
      }
    end
  end
end

Molto bene, ma possiamo fare meglio. La nostra API avrà probabilmente una route per i dati di un singolo user, qualcosa come /v1/users/1, quindi possiamo portarci avanti e contemporaneamente fare DRY del codice attuale:

# app/serializers/v1/users_serializer.rb

module V1
  module UsersSerializer
    include V1::SimpleAwesomeStuffSerializer

    def serialize_user(user = @user)
      {
          id: user.id,
          name: user.name,
      }
    end

    def serialize_users(users = @users)
      {
        users: users.map do |user|
          serialize_user(user).merge(
            {
                awesome_stuffs: user.awesome_stuffs.map do |aww|
                  serialize_awesome_stuff aww
                end
            }
          )
        end
      }
    end
  end
end

Ok, abbiamo “preso due piccioni con una fava”. (i piccioni stanno bene, tranquillo) Come avrai notato è possibile ottenere un ulteriore miglioramento:

# app/serializers/v1/users_serializer.rb

module V1
  module UsersSerializer
    include V1::SimpleAwesomeStuffSerializer

    def serialize_user(user = @user)
      {
          id: user.id,
          name: user.name
      }
    end

    def serialize_users(users = @users)
      {
        users: users.map do |user|
          serialize_user(user).merge(serialize_awesome_stuffs(user.awesome_stuffs))
        end
      }
    end
  end
end

Quello che hai visto è un semplice esempio di cosa si può fare con gli strumenti che abbiamo già sotto mano e vuole essere principalmente uno spunto per chi è costantemente alla ricerca della migliore performance e semplicità.

Siamo arrivati alla fine e spero di non averti annoiato troppo, ma se sei arrivato a questo punto probabilmente no :) Quello che hai visto oggi può piacere o meno, ma personalmente trovo che sia un sistema sicuramente performante che offre pura modularità, estendibilità e riuso del codice.

Sentiti libero di lasciare un commento, ci farebbe davvero piacere sentire il tuo feedback.