#348 The Rails API Gem
- Download:
- source codeProject Files in Zip (32 KB)
- mp4Full Size H.264 Video (27.2 MB)
- m4vSmaller H.264 Video (13.9 MB)
- webmFull Size VP8 Video (13.7 MB)
- ogvFull Size Theora Video (33.5 MB)
Según se van haciendo más populares las aplicaciones basadas en el lado del cliente, hay una cuestión que se hace más y más popular: ¿debemos usar Rails si lo único que queremos es implementar desde el servidor una API en JSON? Si necesitamos desarrollar una API podríamos mirar la gema Rails::API gem del miembro del core de Rails Santiago Pastorino. El README de esta gema entra en detalle describiendo qué partes de Rails nos resultarán útiles si queremos construir una API. Rails es muy modular y esta gema proporciona un generador para adelgazar nuestra aplicación Rails eliminando todo lo que no necesitamos si estamos construyendo una API. En este episodio la vamos a usar con una aplicación muy sencilla para ver cómo funciona.
En marcha con Rails::API
Lo primero que tenemos que hacer es instalar la gema, lo que podemos hacer lanzando la siguiente orden.
$ gem install rails-api
Si estamos usando rbenv tendremos que ejecutar rbenv rehash
para acceder a la orden rails-api
. Se puede utilizar esta orden para crear una aplicación nueva igual que haríamos con rails
. Nuestra aplicación se llamará todo
.
$ rails-api new todo
Esta orden genera una aplicación con lo mínimo necesario para servir una API. Una de las mayores diferencias es que el directorio /app
es mucho más sencillo: no existen los directorios assets
o views
, tan sólo controllers
, mailers
y models
. Si miramos el ApplicationController
que se ha generado veremos que hereda de ActionController::API
que es mucho más ligero que ActionController::Base
.
class ApplicationController < ActionController::API end
Más adelante profundizaremos en las diferencias.
Otra diferencia importante es el Gemfile
. Veremos que incluye la gema rails-api
pero no existe el grupo assets
. También se lista la gema jquery-rails
, pero podemos eliminarla (en futuras versiones de Rails::API no se incluirá).
source 'https://rubygems.org' gem 'rails', '3.2.3' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'rails-api' gem 'sqlite3'
Los generadores funcionan también de manera distinta. Si intentamos generar un andamiaje para un modelo llamado Task
veremos que se generan muchos menos ficheros porque no existen ficheros de plantillas u otros recursos.
$ rails g scaffold task name invoke active_record create db/migrate/20120517174851_create_tasks.rb create app/models/task.rb invoke test_unit create test/unit/task_test.rb create test/fixtures/tasks.yml route resources :tasks, except: :edit invoke scaffold_controller create app/controllers/tasks_controller.rb invoke test_unit create test/functional/tasks_controller_test.rb
Una vez que hemos creado este andamiaje vamos a migrar la base de datos para poder usarlo.
$ rake db:migrate
Examinando el fichero TasksController
que ha sido generado veremos que tan sólo muestra respuestas en JSON.
/app/controllers/tasks_controller.rb
class TasksController < ApplicationController
# GET /tasks
# GET /tasks.json
def index
@tasks = Task.all
render json: @tasks
end
# GET /tasks/1
# GET /tasks/1.json
def show
@task = Task.find(params[:id])
render json: @task
end
# Other actions omitted.
end
Estas aplicaciones no están diseñadas para mostrar una vista en HTML o una interfaz de usuario por lo que los controladores tan sólo devuelven JSON. Si visitamos la ruta /tasks/
con el navegador veremos la salida JSON, que de momento será un array vacío porque todavía no hemos creado ninguna tarea.
Creación de una interfaz de usuario que utiliza la API
Para presentar una interfaz de usuario que cree y gestione tarea tendremos que hacerlo o bien en el directorio /public
o bien completamente fuera de la aplicación. Para nuestros propósitos nos bastará con cambiar los contenidos del fichero index.html
por un código HTML que nos permitirá ver y crear nuevas tareas.
<!DOCTYPE html> <html> <head> <title>To Do List</title> <style type="text/css" media="screen"> html, body { background-color: #4B7399; background-color: white; font-family: Verdana, Helvetica, Arial; font-size: 14px; } a { color: #0000FF; } #container { width: 75%; margin: 0 auto; background-color: #FFF; padding: 20px 40px; border: solid 1px black; margin-top: 20px; } </style> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script> <script type="text/javascript" charset="utf-8"> $(function() { function addTask(task) { $('#tasks').append('<li>' + task.name + '</ul>'); } $('#new_task').submit(function(e) { $.post('/tasks', $(this).serialize(), addTask); this.reset(); e.preventDefault(); }); $.getJSON('/tasks', function(tasks) { $.each(tasks, function() { addTask(this); }); }); }); </script> <body> <div id="container"> <h1>To-Do List</h1> <form id="new_task"> <input type="text" name="task[name]" id="task_name"> <input type="submit" value="Add"> </form> <ul id="tasks"></ul> </div> </body> </html>
Por supuesto deberíamos estructurar todo esto de alguna manera (nosotros lo hemos incluído todo en línea). En la raíz de esta aplicación ya tenemos una página donde podemos añadir elementos a una lista, que se guardarán mediante la API. Podemos recargar la página para recuperar los ficheros guardados con la API.
Y esto es precisamente lo que hace el código jQuery de nuestra página. Invocamos a $.getJSON
para obtener las tareas y $.post
para crear una.
$('#new_task').submit(function(e) { $.post('/tasks', $(this).serialize(), addTask); this.reset(); e.preventDefault(); }); $.getJSON('/tasks', function(tasks) { $.each(tasks, function() { addTask(this); }); });
Por supuesto este ejemplo es deliberadamente simple, pero podríamos utilizar un framework del lado del cliente como Backbone.js.
Manipulación de formatos adicionales
A continuación vamos a ver algunas cosas que el controlador de Rails::API deja fuera. Supongamos que queremos que la API responda a diferentes formatos, para poder devolver por ejemplo una lista de tareas en XML o en JSON. Por lo general añadiríamos un bloque respond_to
como:
def index @tasks = Task.all respond_to do |format| format.json { render json: @tasks } format.xml { render xml: @tasks } end end
Si visitamos http://localhost:3000/tasks.xml
esperaríamos ver el resultado en XML pero lo que vemos es el error undefined method `respond_to’
. En aras a mantener la sencillez, al controlador de Rails::API le faltan ciertas funcionalidades pero si queremosse las podemos añadir muy fácilmente. Tenemos que modificar TasksContoller
e incluir un módulo llamado ActionController::MimeResponds
.
class TasksController < ApplicationController include ActionController::MimeResponds # Actions omitted end
Si quisiéramos que otros controladores también pudiesen devolver XML podríamos incluir el módulo en ApplicationController
. Si ahora recargamos la página veremos que la tarea se muestra como XML.
EL README de RAILS::API incluye la lista de módulos que se pueden incluir. Además de ActionController::MimeResponds
podemos incluir otros para soporte de traducciones, autenticación HTTP básica, etcétera. No es esta la lista completa, sin embargo, para verlos todos tenemos que ver el código para comparar los módulos que se incluyen en Rails::API controller con los que se incluyen en ActionController::Base. Por supuesto, si incluyésemos todos los módulos de ActionController::Base habríamos duplicado su funcionalidad. Así que podemos escoger exactamente lo que queramos.
Sin embargo a veces queremos hacer algo más que tan sólo incluir un módulo en el controlador. Por ejemplo si queremos añadir soporte de cookies tenemos que incluir un módulo así como un middleware de Rack. Lo que nos lleva a un punto muy importante: Rails API no sólo recorta los controladores sino también toda la pila de middlewares. Si vamos a nuestra aplicación Rails y ejecutamos rake middleware
veremos una lista de todos los middlewares que se usan, y veremos que será más corta que la de una aplicación Rails normal.
$ rake middleware use ActionDispatch::Static use Rack::Lock use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007f9704a76118> use Rack::Runtime use ActionDispatch::RequestId use Rails::Rack::Logger use ActionDispatch::ShowExceptions use ActionDispatch::DebugExceptions use ActionDispatch::RemoteIp use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::ConnectionAdapters::ConnectionManagement use ActiveRecord::QueryCache use ActionDispatch::ParamsParser use ActionDispatch::Head use Rack::ConditionalGet use Rack::ETag run Todo::Application.routes
Nos faltan cinco middlewares Si vemos la diferencia con los de una aplicación Rails completa, los que faltan son:
> use Rack::MethodOverride > use ActionDispatch::Cookies > use ActionDispatch::Session::CookieStore > use ActionDispatch::Flash > use ActionDispatch::BestStandardsSupport
Los que se pregunten qué hacen estos middlewares pueden echar un vistazo al episodio 319 para ver los detalles.
Para recuperar alguna de estas funcionalidades, como por ejemplo soporte de cookies tenemos que añadir el middleware e incluir el módulo del controlador, lo que haremos en el fichero de configuración de la aplicación:
module Todo class Application < Rails::Application config.middleware.insert_after ActiveRecord::QueryCache, ActionDispatch::Cookies # Se omite el resto de la clase end end
Como es buena idea mantener el mismo orden que cuando tenemos una aplicación completa, hemos utilizado insert_after
para añadir el middleware de Cookies después de QueryCache
. Ya podemos incluir el módulo Cookies
en TasksController
para gestionar cookies.
class TasksController < ApplicationController include ActionController::MimeResponds include ActionController::Cookies # Contenido omitido end
A continuación tenemos que reiniciar la aplicación para que surtan efecto los cambios. Tras recargar la página, veremos un mensaje de error que nos indica que no se encuentra definido el método helper_method
.
Lo cual nos lleva a un punto muy importante. Algunos módulos dependen de otros, en este caso el módulo Cookies
depende de Helpers
por lo que también tendremos que añadirlo.
class TasksController < ApplicationController include ActionController::MimeResponds include ActionController::Helpers include ActionController::Cookies # Contenido omitido end
Ahora veremos que al recargar la página vuelve a funcionar y ya podemos usar cookies como en una aplicación Rails completa.
Uso de JBuilder y RABL para generar JSON
Discutiremos a continuación otras gemas que nos pueden ayudar a construir una API en JSON. En anteriores episodios hemos repasado Jbuilder y RABL. Estas gemas muestran JSON utilizando plantillas de vista, lo que va contra la idea Rails::API, pero si queremos las podemos usar. Como siempre tenemos que añadir la gema al Gemfile
y luego ejecutar bundle
para instalarlo todo.
gem 'rabl'
Como nuestro directorio /app
no contiene un directorio de vistas tenemos que crearlo para que RABL encuentre allí las plantillas. Tenemos que crear un directorio views/tasks
y en él un fichero llamado index.json.rabl
. En este archivo le tenemos que decir a RABL que muestre las tareas como una colección con atributos id
y name
.
collection @tasks attributes :id, :name
En el controlador tenemos que cambiar el comportamiento de visualización de JSON para que muestre esta plantilla. En una aplicación Rails normal podríamos eliminar la llamada a render
, lo que de forma implícita mostraría la plantilla que coincidiese con la acción que se haya ejecutado.
def index @tasks = Task.all respond_to do |format| format.json { render } format.xml { render xml: @tasks } end end
Tenemos que reiniciar la aplicación una vez más pero tras hacerlo podemos visitar /tasks.json
y veremos la respuesta JSON que ha sido generada por RABL.
Podríamos incluir el módulo ImplicitRender
y no tendríamos que llamar a render
cada vez: añadiendo una plantilla XML podríamos eliminar todo el bloque respond_to
porque se detectaría el formato para mostrar automáticamente la plantilla adecuada.
Pero ya estamos acercándonos peligrosamente a una aplicación Rails completa, gestionando cookies y escogiendo plantillas de forma implícita dependiendo del formato. Si volvemos a necesitar toda esta funcionalidad... ¿merece la pena utilizar Rails::API de entrada? Llegados a este punto sería mejor que estuviéramos usando una aplicación Rails completa, aunque sólo estemos implementado una API. La elección depende, por supuesto, de los requisitos de cada aplicación, y cuánto de la pila de Rails tenemos que utilizar.
Merece la pena leer la sección del README llamada ¿Por qué utilizar Rails para implementar APIs en JSON? que entra en detalle acerca de las capacidades de Rails más allá de servir contenidos en HTML.