Andrés
•
23 June 2024
Antes de continuar construyendo nuestra API REST con Ruby On Rails me gustaría dar un salto hacia atrás para aclarar algunos puntos. Cuando comencé a trabajar como desarrollador había muchas cosas que no tenía claras y que me costó tiempo, esfuerzo, pruebas y errores aprender. Una de esas era ¿Qué era realmente un API y cuál es la mejor forma de construir una? ¿Cuáles rutas definir y qué respuestas dar?
Ahora esto me parece obvio y básico, pero creo que vale la pena repasar. Según IBM un API REST es:
Una API REST (también llamada API RESTful o API web RESTful) es una interfaz de programación de aplicaciones (API) que se ajusta a los principios de diseño del estilo arquitectónico de transferencia de estado representacional (REST). Las API REST proporcionan una forma flexible y ligera de integrar aplicaciones y conectar componentes en arquitecturas de microservicios.
Lo único que se debe respetar cuando diseñas una API REST deben ser los 6 principios REST:
¿Y cómo funciona esto? Probablemente si estás leyendo esto es porque ya sabes. Solicitudes HTTP para realizar funciones de base de datos estándar como crear, leer, actualizar y eliminar (CRUD) sobre un recurso. Y aquí mi recomendación y consejo: siempre intenta realizar tus APIs orientadas a recursos y no a acciones. Con Ruby On Rails esto no es difícil, pero siempre está la tentación de realizar endpoints como POST /publishArticle
en vez de realizar PUT /article/:article_id
con el contenido correcto.
Con esto aclarado continuamos con nuestra serie de posts, segundo capítulo:
Ahora bien, continuando con nuestro ejemplo arreglaremos las rutas. Usaremos rutas anidadas para los recursos que dependen de Projects
utilizando la opción shallow para crear solo las rutas necesarias para identificar el recurso y evitar el anidamiento profundo.
# config/routes.rb
Rails.application.routes.draw do
get 'up' => 'rails/health#show', :as => :rails_health_check
resources :projects, shallow: true do
resources :tasks
resources :roles
end
end
Eliminé comentarios y las ordené un poco. Si usamos el comando rails routes
, estaremos viendo nuestras rutas apuntando a nuestros métodos de controlador de esta forma:
projects GET /projects(.:format) projects#index
POST /projects(.:format) projects#create
project GET /projects/:id(.:format) projects#show
PATCH /projects/:id(.:format) projects#update
PUT /projects/:id(.:format) projects#update
DELETE /projects/:id(.:format) projects#destroy
project_tasks GET /projects/:project_id/tasks(.:format) tasks#index
POST /projects/:project_id/tasks(.:format) tasks#create
task GET /tasks/:id(.:format) tasks#show
PATCH /tasks/:id(.:format) tasks#update
PUT /tasks/:id(.:format) tasks#update
DELETE /tasks/:id(.:format) tasks#destroy
project_roles GET /projects/:project_id/roles(.:format) roles#index
POST /projects/:project_id/roles(.:format) roles#create
role GET /roles/:id(.:format) roles#show
PATCH /roles/:id(.:format) roles#update
PUT /roles/:id(.:format) roles#update
DELETE /roles/:id(.:format) roles#destroy
Si ejecutamos rails t
, nos encontraremos con varios errores. El primer problema que debemos resolver es que Rails, hasta el momento, ha generado nuestras factorías con FactoryBot
, pero no las está utilizando automáticamente para crear los registros de prueba en el bloque setup
de cada archivo de test de controladores. En su lugar, está utilizando las fixtures para obtener un objeto de prueba. Sin embargo, estas fixtures no fueron creadas porque el comportamiento cambió cuando instalamos FactoryBot
; ahora se utilizan factorías en lugar de fixtures. Para solucionar esto, necesitamos reemplazar la línea número 5 de nuestros tests de controladores de la siguiente forma:
# test/controllers/projects_controllers_test.rb
- @project = projects(:one)
+ @project = FactoryBot.create(:project)
y lo mismo para los otros dos, pero agregaremos una línea más al setup
con un objeto project
que usaremos más adelante en las rutas:
# test/controllers/roles_controllers_test.rb
- @role = roles(:one)
+ @role = FactoryBot.create(:role)
+ @project = @role.project
# test/controllers/tasks_controllers_test.rb
- @task = tasks(:one)
+ @task = FactoryBot.create(:task)
+ @project = @task.project
Ahora todavía deberíamos tener 4 tests fallando y esto es debido a que cambiamos la estructura de las rutas. Para corregir eso debemos usar los nuevos helpers creados con las rutas anidadas para las acciones index
y create
en los controladores RolesController
y TasksController
. Específicamente, debes cambiar roles_url
y tasks_url
de las líneas número 9 y 15 por project_roles_url(@project)
y project_tasks_url(@project)
respectivamente.
Cuando realices eso correctamente, podrás correr tus tests y obtener un resultado como este:
15 runs, 27 assertions, 0 failures, 0 errors, 0 skips
La serialización de datos se refiere al proceso de convertir objetos de datos (como instancias de modelos ActiveRecord) en formatos que pueden ser fácilmente transmitidos y entendidos por diferentes sistemas, en nuestro caso, transformarlos en formato JSON.
En Rails, nuestros modelos ya incluyen por defecto el módulo ActiveModel::Serializers::JSON, que les permite serializar todos los atributos (se pueden filtrar) a un Hash y, por ende, a un objeto JSON. Esto es lo que está ocurriendo de forma predefinida en nuestros métodos de controladores creados con scaffold
. Sin embargo, necesitamos ir un poco más allá, necesitamos más personalización y flexibilidad. Para esto propongo el uso de Blueprinter, que es una opción confiable y flexible.
Blueprinter es un presentador de objetos JSON para Ruby que toma objetos de negocio y los descompone en simples hashes y los serializa a JSON. Puede utilizarse en Rails en lugar de otros serializadores (como JBuilder o ActiveModelSerializers). Está diseñado para ser sencillo, directo y eficaz. Se basa en gran medida en la idea de vistas que, de forma similar a las vistas de Rails, son formas de predefinir la salida de datos en diferentes contextos.
Entonces, la gema ya la instalamos en el capítulo anterior. Ahora solo nos queda crear nuestras clases serializadoras. Para esto vamos a crear una carpeta en la ruta app/blueprints/
y dentro incluiremos 4 archivos (uno para cada modelo) con el siguiente contenido:
# app/blueprints/project_blueprint.rb
class ProjectBlueprint < Blueprinter::Base
identifier :id
fields :name, :description
view :with_tasks do
association :tasks, blueprint: TaskBlueprint
end
end
# app/blueprints/task_blueprint.rb
class TaskBlueprint < Blueprinter::Base
identifier :id
fields :title, :description, :status
end
# app/blueprints/role_blueprint.rb
class RoleBlueprint < Blueprinter::Base
identifier :id
fields :role
association :user, blueprint: UserBlueprint
end
# app/blueprints/user_blueprint.rb
class UserBlueprint < Blueprinter::Base
identifier :id
fields :email
end
Ahora que ya tenemos nuestros serializadores listos, debemos implementarlos en nuestros métodos de controladores. Para esto, simplemente te explicaré cómo se usa, pero no mostraré cada cambio que debes hacer porque son varias líneas que tocar. Identifica cada línea de código en los controladores que tenga la palabra render
. Esto especifica la respuesta al cliente, en este caso un JSON con el objeto o los objetos que, como ya dije, se serializan por defecto con ActiveModel::Serializers
. Pero nosotros cambiaremos eso. Por ejemplo, para un Project
, escribiremos: render json: ProjectBlueprint.render_as_json(@project)
. De esta forma, Blueprint será el encargado de serializar el objeto en lugar de Serializers.
Otro ejemplo, para nuestro método show
, tal vez queramos mostrar un objeto más completo. Para eso podemos hacerlo así: render json: ProjectBlueprint.render_as_json(@project, view: :with_tasks)
, y de esa manera no solo retornaremos el proyecto, sino también sus tareas.
Después de realizar los cambios correspondientes, puedes volver a probar que todo está funcionando correctamente ejecutando rails t
.
Con esto ya tendremos nuestros endpoints funcionando, las rutas tienen sentido al usar anidación, los serializadores nos dan un mayor control sobre qué datos exponer y cuáles ocultar dependiendo del método y, en un futuro, de los permisos. Y lo más importante, nuestros tests están funcionando. Validan la creación, obtención, modificación y eliminación de nuestros datos.
Si hay algún punto que no expresé correctamente o me salté algo hasta ahora, por favor escríbeme. Además, te agrego la URL del repositorio donde iré subiendo el código actualizado para que lo vayas revisando:
Repo: https://github.com/a-chacon/api-project-management-example