Elaborando APIs Con Rails
Spanish (Español) translation by Rafael Chavarría (you can also view the original English article)
Hoy en día es una práctica común depender de APIs (application programming interfaces). No solo grandes servicios como Facebook y Twitter las emplean--Las APIs son muy populares debido a la propagación de frameworks del lado del cliente como React, Angular y muchos otros. Ruby on Rails está siguiendo esta tendencia y la última versión presenta una nueva característica permitiéndote crear aplicaciones de solo API.
Inicialmente esta funcionalidad estaba empacada en una gema separada llamada rails-api, pero desde la liberación de Rails 5, es ahora parte del núcleo del framework. Esta característica con ActionCable fue probablemente la más anticipada, así que hoy la vamos a discutir.
Este artículo cubre cómo crear aplicaciones solo-API de Rails y explica cómo estructurar tus rutas y controladores, responder con el formato JSON, agregar serializadores y configurar CORS (Cross Origin Resource Sharing). También aprenderás sobre algunas opciones para asegurar la API y protegerla de abusos.
La fuente para este artículo está disponible en GitHub.
Creando una Aplicación Solo-API
Para comenzar, ejecuta el siguiente comando:
1 |
rails new RailsApiDemo --api
|
Va a crear una nueva aplicación de Rails solo-API llamado RailsApiDemo. No olvides que el soporte para la opción --api fue agregada solo en Rails 5, así que asegúrate de que tienes esta o una versión más nueva instalada.
Abre el Gemfile y nota que es mucho más pequeño de lo habitual: gemas como coffee-rails, turbolinks y sass-rails no están.
El archivo config/application.rb contiene una nueva línea:
1 |
config.api_only = true |
Significa que Rails va a cargar un conjunto más pequeño de intermediario: por ejemplo, no hay soporte de cookies ni sesiones. Más aún, si tratas de generar un andamio, las vistas y recursos no serán creados. Actualmente, si revisas el directorio views/layouts, notarás que el archivo application.html.erb también falta.
Otra diferencia importante es que ApplicationController hereda de ActionController::API, no ActionController::Base.
Esto es prácticamente todo, esta es la aplicación básica de Rails que has visto muchas veces. Ahora agreguemos un par de modelos para que tengamos algo con que trabajar:
1 |
rails g model User name:string |
2 |
rails g model Post title:string body:text user:belongs_to |
3 |
rails db:migrate |
Nada elegante está pasando aquí: una publicación con un título y un cuerpo que pertenece a un usuario.
Asegúrate de que las asociaciones apropiadas están configuradas y también proporciona revisiones simples de validación:
models/user.rb
1 |
has_many :posts |
2 |
|
3 |
validates :name, presence: true |
models/post.rb
1 |
belongs_to :user |
2 |
|
3 |
validates :title, presence: true |
4 |
validates :body, presence: true |
¡Brillante! El siguiente paso es cargar un par de registros de ejemplo en las tablas recién creadas.
Cargando Datos Demostrativos
La manera más fácil de cargar alguna información es utilizando el archivo seeds.rb dentro del directorio db. Sin embargo, soy flojo (como muchos programadores) y no quiero pensar en cualquier contenido de ejemplo. De ahí, porqué no sacamos ventaja de la faker gem que puede producir datos aleatorios de varios tipos: nombres, emails, palabras hipsters, textos "lorem ipsum" y mucho más.
Gemfile
1 |
group :development do |
2 |
gem 'faker' |
3 |
end |
Instala la gema:
1 |
bundle install
|
Ahora modifica el seeds.rb:
db/seeds.rb
1 |
5.times do |
2 |
user = User.create({name: Faker::Name.name})
|
3 |
user.posts.create({title: Faker::Book.title, body: Faker::Lorem.sentence})
|
4 |
end |
Por último, carga tus datos:
1 |
rails db:seed |
Respondiendo Con JSON
Ahora, por supuesto, necesitamos algunas rutas y controladores para elaborar nuestra API. Es una práctica común anidar las rutas de la API bajo la ruta api/. También, los desarrolladores usualmente proporcionan la versión de la API en la ruta, por ejemplo api/v1/. Después, si algunos cambios importantes han sido introducidos, puedes simplemente crear un nuevo namespace (v2) y un controlador separado.
Aquí está como pueden lucir tus rutas:
config/routes.rb
1 |
namespace 'api' do |
2 |
namespace 'v1' do |
3 |
resources :posts |
4 |
resources :users |
5 |
end |
6 |
end |
Esto genera rutas como:
1 |
api_v1_posts GET /api/v1/posts(.:format) api/v1/posts#index |
2 |
POST /api/v1/posts(.:format) api/v1/posts#create |
3 |
api_v1_post GET /api/v1/posts/:id(.:format) api/v1/posts#show |
Podrías usar un método scope en vez del namespace, pero entonces por defecto buscará el UsersController y PostsController dentro del directorio controllers, no dentro de controllers/api/v1, así que ten cuidado.
Crea la carpeta api con el directorio anidado v1 dentro de controllers. Complétalo con tus controladores:
controllers/api/v1/users_controller.rb
1 |
module Api |
2 |
module V1 |
3 |
class UsersController < ApplicationController |
4 |
end
|
5 |
end
|
6 |
end
|
controllers/api/v1/posts_controller.rb
1 |
module Api |
2 |
module V1 |
3 |
class PostsController < ApplicationController |
4 |
end
|
5 |
end
|
6 |
end
|
Nota que no solo tienes que anidar el archivo del controlador bajo la tura api/v1, sino que también la clase misma tiene que tener namespace dentro de los módulo Api y V1.
La siguiente pregunta es ¿cómo responder apropiadamente con los datos formateados en JSON? En este artículo probaremos estas soluciones: las gemas jBuilder y active_model_serializers. Así que antes de proceder a la siguiente sección, ponlos en el Gemfile:
Gemfile
1 |
gem 'jbuilder', '~> 2.5' |
2 |
gem 'active_model_serializers', '~> 0.10.0' |
Entonces ejecuta:
1 |
bundle install
|
Usando la Gema jBuilder
jBuilder es una gema popular mantenida por el equipo de Rails que proporciona un simple DLS (domain-specific language) permitiéndote definir estructuras JSON en tus vistas.
Supón que queremos mostrar todas las publicaciones cuando el usuario inicia la acción index:
controllers/api/v1/posts_controller.rb
1 |
def index |
2 |
@posts = Post.order('created_at DESC')
|
3 |
end |
Todo lo que necesitas hacer es crear la vista llamada como la acción correspondiente con la extensión .json.jbuilder. Nota que la vista debe ser colocada bajo la ruta api/v1 también:
views/api/v1/posts/index.json.jbuilder
1 |
json.array! @posts do |post| |
2 |
json.id post.id |
3 |
json.title post.title |
4 |
json.body post.body |
5 |
end
|
json.array! traversa el arreglo @posts. json.id, json.title y json.body generan las llaves con los nombres correspondientes estableciendo los argumentos como los valores. Si navegas a http://localhost:3000/api/v1/posts.json, verás un resultado similar a este:
1 |
[
|
2 |
{"id": 1, "title": "Title 1", "body": "Body 1"}, |
3 |
{"id": 2, "title": "Title 2", "body": "Body 2"} |
4 |
]
|
¿Qué si queremos mostrar el autor de cada publicación también? Es simple:
1 |
json.array! @posts do |post| |
2 |
json.id post.id |
3 |
json.title post.title |
4 |
json.body post.body |
5 |
json.user do |
6 |
json.id post.user.id |
7 |
json.name post.user.name |
8 |
end
|
9 |
end
|
El resultado cambiará a:
1 |
[
|
2 |
{"id": 1, "title": "Title 1", "body": "Body 1", "user": {"id": 1, "name": "Username"}} |
3 |
]
|
Los contenidos de los archivos .jbuilder es código plano Ruby, así que podrías utilizar todas operaciones básicas de manera usual.
Nota que jBuilder soporta parciales como cualquier vista ordinaria de Rails, así que también podrías decir:
1 |
json.partial! partial: 'posts/post', collection: @posts, as: :post |
y después crear el archivo views/api/v1/posts/_post.json.jbuilder con los siguientes contenidos:
1 |
json.id post.id |
2 |
json.title post.title |
3 |
json.body post.body |
4 |
json.user do |
5 |
json.id post.user.id |
6 |
json.name post.user.name |
7 |
end
|
Así qué, como ves, jBuilder es sencillo y conveniente. Sin embargo, como una alternativa, deberías apegarte a los serializadores, así que discutámoslos en la siguiente sección.
Usando Serializadores
La gema rails_model_serializers fue creada por un equipo que inicialmente administró la rails-api. Como se declara en la documentación, rails_model_serializers trae convención sobre configuración a tu generación de JSON. Básicamente, defines cuales campos deberían ser usados en la serialización (eso es, generación JSON).
Aquí está nuestro primer serializador:
serializers/post_serializer.rb
1 |
class PostSerializer < ActiveModel::Serializer |
2 |
attributes :id, :title, :body |
3 |
end
|
Aquí decimos que todos estos campos deberían estar presentes en el JSON resultante. Ahora métodos como to_json y as_json llamados sobre una publicación usarán esta configuración y regresarán el contenido apropiado.
Para verlo en acción, modifica la acción index así:
controllers/api/v1/posts_controller.rb
1 |
def index |
2 |
@posts = Post.order('created_at DESC')
|
3 |
|
4 |
render json: @posts |
5 |
end |
as_json será llamado automáticamente sobre el objeto @posts.
¿Y los usuarios? Los serializadores te permiten indicar relaciones, justo como hacen los modelos. Lo que es más, los serializadores pueden ser anidados:
serializers/post_serializer.rb
1 |
class PostSerializer < ActiveModel::Serializer |
2 |
attributes :id, :title, :body |
3 |
belongs_to :user |
4 |
|
5 |
class UserSerializer < ActiveModel::Serializer |
6 |
attributes :id, :name |
7 |
end
|
8 |
end
|
Ahora cuando serializas el post, este contendrá automáticamente la llave de user anidada con su id y nombre. Si después creas un serializador separado para el usuario con el atributo :id excluído:
serializers/post_serializer.rb
1 |
class UserSerializer < ActiveModel::Serializer |
2 |
attributes :name |
3 |
end
|
entonces @user.as_json no regresará el id de usuario. Aun así, @post.as_json regresará tanto el nombre y id del usuario, así que tenlo en mente.
Asegurando la API
En muchos casos, no queremos que nadie ejecute ninguna acción usando la API. Así que presentemos una revisión simple de seguridad y forcemos a los usuarios a enviar sus tokens cuando crean y borran publicaciones.
El token tendrá un ciclo de vida ilimitado y será creado sobre el registro del usuario. Primero que todo, agrega una nueva columna token a la tabla users.
1 |
rails g migration add_token_to_users token:string:index |
Este índice debería garantizar unicidad ya que no puede haber dos usuarios con el mismo token:
db/migrate/xyz_add_token_to_users.rb
1 |
add_index :users, :token, unique: true |
Aplica la migración:
1 |
rails db:migrate |
Ahora agrega el retroceso before_save:
models/user.rb
1 |
before_create -> {self.token = generate_token}
|
El método privado generate_token creará un token en un ciclo interminable y revisará si es único o no. Tan pronto como un token único sea encontrado, regrésalo:
models/user.rb
1 |
private |
2 |
|
3 |
def generate_token |
4 |
loop do |
5 |
token = SecureRandom.hex |
6 |
return token unless User.exists?({token: token})
|
7 |
end |
8 |
end |
Podrías usar otro algoritmo para generar el token, por ejemplo basado en el hash MD5 para el nombre de usuario y algo de salt.
Registro de Usuario
Por supuesto, también necesitamos permitir a los usuarios registrarse, porque de otro modo no serán capaces de obtener su token. No quiero introducir ninguna vista HTML en nuestra aplicación, así que en su lugar agrega un nuevo método API:
controllers/api/v1/users_controller.rb
1 |
def create |
2 |
@user = User.new(user_params) |
3 |
if @user.save |
4 |
render status: :created |
5 |
else |
6 |
render json: @user.errors, status: :unprocessable_entity |
7 |
end |
8 |
end |
9 |
|
10 |
private |
11 |
|
12 |
def user_params |
13 |
params.require(:user).permit(:name) |
14 |
end |
Es una buena idea regresar códigos de estado HTTP significativos de manera que los desarrolladores entiendan exactamente qué está pasando. Ahora podrías ya sea proporcionar un nuevo serializador para los usuarios o apegarse con un archivo .json.jbuilder. Prefiero la última variante (es por eso que no paso la opción :json al método render), pero eres libre de elegir cualquiera de ellos. Nota, sin embargo, que el token no debe ser siempre serializado, por ejemplo cuando regresas una lista de todos los usuarios--¡debería ser mantenido seguro!
views/api/v1/users/create.json.jbuilder
1 |
json.id @user.id |
2 |
json.name @user.name |
3 |
json.token @user.token |
El siguiente paso es probar si todo está trabajando propiamente. Deberías ya sea usar el comando curl o escribir algo de código Ruby. Ya que este artículo es sobre Ruby, iré con la opción de código.
Probando el Registro de Usuario
Para ejecutar una petición HTTP, emplearemos la gema Faraday, la cuál proporciona una interfaz común sobre varios adaptadores (por defecto es Net::HTTP). Crea un archivo Ruby por separado, incluye Faraday y configura el cliente:
api_client.rb
1 |
require 'faraday' |
2 |
|
3 |
client = Faraday.new(url: 'http://localhost:3000') do |config| |
4 |
config.adapter Faraday.default_adapter |
5 |
end
|
6 |
|
7 |
response = client.post do |req| |
8 |
req.url '/api/v1/users' |
9 |
req.headers['Content-Type'] = 'application/json' |
10 |
req.body = '{ "user": {"name": "test user"} }' |
11 |
end
|
Todas estas opciones son bastante claras: elegimos el adaptador por defecto, establecemos la URL de petición a http://localhost:300/api/v1/users, cambiamos el tipo de contenido a application/json y proporcionamos el cuerpo de nuestra petición.
La respuesta del servidor va a contener JSON, así que para pasarlo usaré la gema Oj.
api_client.rb
1 |
require 'oj' |
2 |
|
3 |
# client here...
|
4 |
|
5 |
puts Oj.load(response.body) |
6 |
puts response.status |
Aparte de la respuesta analizada, también muestro el código de estado para propósitos de depuración.
Ahora puedes simplemente ejecutar este script:
1 |
ruby api_client.rb |
y almacenar el token recibido en alguna parte--lo usaremos en la siguiente sección.
Autenticando Con el Token
Para hacer cumplir la autentificación de token, puede ser usado el método authenticate_or_request_with_http_token. Es una parte del módulo ActionController::HttpAuthentication::Token::ControllerMethods, así que no olvides incluirlo:
controllers/api/v1/posts_controller.rb
1 |
class PostsController < ApplicationController |
2 |
include ActionController::HttpAuthentication::Token::ControllerMethods |
3 |
# ... |
4 |
end
|
Agrega un nuevo before_action y el método correspondiente:
controllers/api/v1/posts_controller.rb
1 |
before_action :authenticate, only: [:create, :destroy] |
2 |
|
3 |
# ... |
4 |
|
5 |
private |
6 |
|
7 |
# ... |
8 |
|
9 |
def authenticate |
10 |
authenticate_or_request_with_http_token do |token, options| |
11 |
@user = User.find_by(token: token) |
12 |
end |
13 |
end |
Ahora si el token no es establecido o si un usuario con dicho token no puede ser encontrado, será regresado un error 401, interrumpiendo la ejecución de la acción.
Nota que la comunicación entre el cliente y el servidor tien que ser hecha sobre HTTPS, porque de otro modo los tokens podrían ser fácilmente falsificados. Por supuesto, la solución proporcionado no es ideal, y en muchos casos es preferible emplear el protocolo OAuth para autenticación. Hay por lo menos dos gemas que simplfican enormemente el proceso de soportat esta característica: Doorkeeper y oPRO.
Creando una Publicación
Para ver nuestra autenticación en acción, agrega la acción create al PostsController:
controllers/api/v1/posts_controller.rb
1 |
def create |
2 |
@post = @user.posts.new(post_params) |
3 |
if @post.save |
4 |
render json: @post, status: :created |
5 |
else |
6 |
render json: @post.errors, status: :unprocessable_entity |
7 |
end |
8 |
end |
Tomamos ventaja del serializador aquí para mostrar el JSON apropiado. El @user ya fue establecido dentro de before_action.
Ahora prueba todo usando este simple código:
api_client.rb
1 |
client = Faraday.new(url: 'http://localhost:3000') do |config| |
2 |
config.adapter Faraday.default_adapter |
3 |
config.token_auth('127a74dbec6f156401b236d6cb32db0d') |
4 |
end
|
5 |
|
6 |
response = client.post do |req| |
7 |
req.url '/api/v1/posts' |
8 |
req.headers['Content-Type'] = 'application/json' |
9 |
req.body = '{ "post": {"title": "Title", "body": "Text"} }' |
10 |
end
|
Reemplaza el argumento pasado al token_auth con el token recibido en el registro, y ejecuta el script.
1 |
ruby api_client.rb |
Borrando una Publicación
El borrado de una publicación es hecho de la misma manera. Agrega la acción destroy:
controllers/api/v1/posts_controller.rb
1 |
def destroy |
2 |
@post = @user.posts.find_by(params[:id]) |
3 |
if @post |
4 |
@post.destroy |
5 |
else |
6 |
render json: {post: "not found"}, status: :not_found
|
7 |
end |
8 |
end |
Solo permitimos a los usuarios destruir las publicaciones que les pertenecen. Si la publicación es removida satisfactoriamente, el código de estado 204 (sin contenido) será regresado. De manera alternativa, podrías responder con el id de la publicación que fue borrada ya que aún estará disponible desde la memoria.
Aquí está la pieza de código para probar esta nueva característica:
api_client.rb
1 |
response = client.delete do |req| |
2 |
req.url '/api/v1/posts/6' |
3 |
req.headers['Content-Type'] = 'application/json' |
4 |
end
|
Reemplaza el id de la publicación con un número que funcione para ti.
Configurando CORS
Si quieres habilitar otros servicios web para acceder a tu API (del lado del cliente), entonces CORS (Cross-Origin Resource Sharing) debería ser propiamente configurado. Básicamente, CORS permite a las aplicaciones web enviar peticiones AJAX a servicios de terceros. Afortunadamente, hay una gema llamada rack-cors que nos habilita fácilmente para configurar todo. Agrégalo al Gemfile:
Gemfile
1 |
gem 'rack-cors' |
Instálalo:
1 |
bundle install
|
Y después proporciona la configuración dentro del archivo config/initializers/cors.rb. Actualmente, este archivo ya está creado por ti y contiene un uso de ejemplo. También puedes encontrar documentación bastante detallada en la página de la gema.
La siguiente configuración, por ejemplo, permitirá a cualquiera acceder a tu API usando cualquier método:
config/initializers/cors.rb
1 |
Rails.application.config.middleware.insert_before 0, Rack::Cors do |
2 |
allow do |
3 |
origins '*' |
4 |
|
5 |
resource '/api/*', |
6 |
headers: :any, |
7 |
methods: [:get, :post, :put, :patch, :delete, :options, :head] |
8 |
end |
9 |
end |
Previniendo Abuso
La última cosa que voy a mencionar en esta guía es cómo proteger tu API de abuso o ataques DDoS. Hay una buena gema llamada rack-attack (creada por la gente desde Kickstarter) que te permite poner en lista negra o blanca a clientes, previniendo la inundación de un servidor con peticiones, y más.
Pon la gema en Gemfile:
Gemfile
1 |
gem 'rack-attack' |
Instala:
1 |
bundle install
|
Y después proporciona configuración dentro del archivo de inicialización rack_attack.rb La documentación de la gema lista todas las opciones disponibles y sugiere algunos casos de uso. Aquí está la muestra de la configuración que restringe a cualquiera excepto a ti de acceder al servicio y limita el número máximo de peticiones a 5 por segundo:
config/initializers/rack_attack.rb
1 |
class Rack::Attack |
2 |
safelist('allow from localhost') do |req|
|
3 |
# Requests are allowed if the return value is truthy |
4 |
'127.0.0.1' == req.ip || '::1' == req.ip |
5 |
end |
6 |
|
7 |
throttle('req/ip', :limit => 5, :period => 1.second) do |req|
|
8 |
req.ip |
9 |
end |
10 |
end |
Otra cosa que necesita ser hecha es incluir RackAttack como un intermediario:
config/application.rb
1 |
config.middleware.use Rack::Attack |
Conclusión
Hemos llegado al final de este artículo. ¡Espero que ahora te sientas más confiado sobre elaborar APIs con Rails! Nota que esta no es la única opción disponible--otra solución popular que lleva un tiempo es el framework Grape, así que podrías estar interesado en revisarlo también.
No dudes en publicar tus preguntas si algo no te pareció claro. Te agradezco por permanecer conmigo, ¡y feliz codificación!



