Cómo Sequel y Sinatra resuelven el problema de la API de Ruby
Publicado: 2022-03-11Introducción
En los últimos años, la cantidad de marcos de aplicación de una sola página de JavaScript y aplicaciones móviles ha aumentado sustancialmente. Esto impone una demanda correspondientemente mayor de API del lado del servidor. Dado que Ruby on Rails es uno de los marcos de desarrollo web más populares de la actualidad, es una opción natural entre muchos desarrolladores para crear aplicaciones API de back-end.
Sin embargo, mientras que el paradigma arquitectónico de Ruby on Rails hace que sea muy fácil crear aplicaciones de API de back-end, usar Rails solo para la API es una exageración. De hecho, es exagerado hasta el punto de que incluso el equipo de Rails ha reconocido esto y, por lo tanto, ha introducido un nuevo modo solo API en la versión 5. Con esta nueva característica en Ruby on Rails, crear aplicaciones solo API en Rails se volvió aún más fácil. y una opción más viable.
Pero también hay otras opciones. Los más notables son dos gemas muy maduras y poderosas, que en combinación brindan herramientas poderosas para crear API del lado del servidor. Ellos son Sinatra y Sequel.
Ambas gemas tienen un conjunto de funciones muy rico: Sinatra sirve como lenguaje específico de dominio (DSL) para aplicaciones web, y Sequel sirve como capa de mapeo relacional de objetos (ORM). Entonces, echemos un breve vistazo a cada uno de ellos.
Sinatra
Sinatra es un marco de aplicación web basado en Rack. The Rack es una conocida interfaz de servidor web de Ruby. Lo utilizan muchos marcos, como Ruby on Rails, por ejemplo, y es compatible con muchos servidores web, como WEBrick, Thin o Puma. Sinatra proporciona una interfaz mínima para escribir aplicaciones web en Ruby, y una de sus características más atractivas es la compatibilidad con componentes de software intermedio. Estos componentes se encuentran entre la aplicación y el servidor web y pueden monitorear y manipular solicitudes y respuestas.
Para utilizar esta función de Rack, Sinatra define DSL interno para crear aplicaciones web. Su filosofía es muy simple: las rutas se representan mediante métodos HTTP, seguidas de una ruta que coincide con un patrón. Un bloque Ruby dentro del cual se procesa la solicitud y se forma la respuesta.
get '/' do 'Hello from sinatra' end
El patrón de coincidencia de ruta también puede incluir un parámetro con nombre. Cuando se ejecuta el bloque de ruta, se pasa un valor de parámetro al bloque a través de la variable params
.
get '/players/:sport_id' do # Parameter value accessible through params[:sport_id] end
Los patrones coincidentes pueden usar el operador splat *
que hace que los valores de los parámetros estén disponibles a través de params[:splat]
.
get '/players/*/:year' do # /players/performances/2016 # Parameters - params['splat'] -> ['performances'], params[:year] -> 2016 end
Este no es el final de las posibilidades de Sinatra relacionadas con la coincidencia de rutas. Puede utilizar una lógica de coincidencia más compleja mediante expresiones regulares, así como comparadores personalizados.
Sinatra comprende todos los verbos HTTP estándar necesarios para crear una API REST: Obtener, Publicar, Poner, Parchar, Eliminar y Opciones. Las prioridades de ruta están determinadas por el orden en que se definen, y la primera ruta que coincide con una solicitud es la que atiende esa solicitud.
Las aplicaciones de Sinatra se pueden escribir de dos maneras; utilizando el estilo clásico o modular. La principal diferencia entre ellos es que, con el estilo clásico, solo podemos tener una aplicación de Sinatra por proceso de Ruby. Otras diferencias son tan pequeñas que, en la mayoría de los casos, se pueden ignorar y se pueden usar las configuraciones predeterminadas.
Enfoque clásico
La implementación de la aplicación clásica es sencilla. Solo tenemos que cargar Sinatra e implementar controladores de ruta:
require 'sinatra' get '/' do 'Hello from Sinatra' end
Al guardar este código en el archivo demo_api_classic.rb
, podemos iniciar la aplicación directamente ejecutando el siguiente comando:
ruby demo_api_classic.rb
Sin embargo, si la aplicación se implementará con controladores de Rack, como Passenger, es mejor iniciarla con el archivo config.ru
de configuración de Rack.
require './demo_api_classic' run Sinatra::Application
Con el archivo config.ru
en su lugar, la aplicación se inicia con el siguiente comando:
rackup config.ru
Enfoque modular
Las aplicaciones modulares de Sinatra se crean subclasificando Sinatra::Base
o Sinatra::Application
:
require 'sinatra' class DemoApi < Sinatra::Application # Application code run! if app_file == $0 end
La declaración que comienza con run!
se utiliza para iniciar la aplicación directamente, con ruby demo_api.rb
, al igual que con la aplicación clásica. Por otro lado, si la aplicación se implementará con Rack, el contenido de los controladores de rackup.ru
debe ser:
require './demo_api' run DemoApi
Continuación
Sequel es la segunda herramienta de este conjunto. A diferencia de ActiveRecord, que es parte de Ruby on Rails, las dependencias de Sequel son muy pequeñas. Al mismo tiempo, tiene muchas funciones y puede usarse para todo tipo de tareas de manipulación de bases de datos. Con su lenguaje específico de dominio simple, Sequel libera al desarrollador de todos los problemas relacionados con el mantenimiento de conexiones, la construcción de consultas SQL, la obtención de datos (y el envío de datos) a la base de datos.
Por ejemplo, establecer una conexión con la base de datos es muy sencillo:
DB = Sequel.connect(adapter: :postgres, database: 'my_db', host: 'localhost', user: 'db_user')
El método de conexión devuelve un objeto de base de datos, en este caso, Sequel::Postgres::Database
, que se puede usar para ejecutar SQL sin formato.
DB['select count(*) from players']
Alternativamente, para crear un nuevo objeto de conjunto de datos:
DB[:players]
Ambas declaraciones crean un objeto de conjunto de datos, que es una entidad Sequel básica.
Una de las características más importantes del conjunto de datos de Sequel es que no ejecuta consultas de inmediato. Esto hace posible almacenar conjuntos de datos para su uso posterior y, en la mayoría de los casos, encadenarlos.
users = DB[:players].where(sport: 'tennis')
Entonces, si un conjunto de datos no llega a la base de datos de inmediato, la pregunta es, ¿cuándo lo hará? Sequel ejecuta SQL en la base de datos cuando se utilizan los llamados "métodos ejecutables". Estos métodos son, por nombrar algunos, all
, each
, map
, first
y last
.
Sequel es extensible, y su extensibilidad es el resultado de una decisión arquitectónica fundamental para construir un pequeño núcleo complementado con un sistema de complementos. Las características se agregan fácilmente a través de complementos que, en realidad, son módulos de Ruby. El complemento más importante es el complemento Model
. Es un complemento vacío que no define ninguna clase o método de instancia por sí mismo. En cambio, incluye otros complementos (submódulos) que definen métodos de conjunto de datos de clase, instancia o modelo. El complemento Model permite el uso de Sequel como la herramienta de mapeo relacional de objetos (ORM) y, a menudo, se lo conoce como el "complemento base".
class Player < Sequel::Model end
El modelo Sequel analiza automáticamente el esquema de la base de datos y configura todos los métodos de acceso necesarios para todas las columnas. Asume que el nombre de la tabla es plural y es una versión subrayada del nombre del modelo. En caso de que sea necesario trabajar con bases de datos que no siguen esta convención de nomenclatura, el nombre de la tabla se puede establecer explícitamente cuando se define el modelo.
class Player < Sequel::Model(:player) end
Entonces, ahora tenemos todo lo que necesitamos para comenzar a construir la API de back-end.
Construyendo la API
Estructura del código
A diferencia de Rails, Sinatra no impone ninguna estructura de proyecto. Sin embargo, dado que siempre es una buena práctica organizar el código para facilitar el mantenimiento y el desarrollo, también lo haremos aquí, con la siguiente estructura de directorios:

project root |-config |-helpers |-models |-routes
La configuración de la aplicación se cargará desde el archivo de configuración YAML para el entorno actual con:
Sinatra::Application.config_file File.join(File.dirname(__FILE__), 'config', "#{Sinatra::Application.settings.environment}_config.yml")
De forma predeterminada, el valor de Sinatra::Applicationsettings.environment
es development,
y se cambia configurando la variable de entorno RACK_ENV
.
Además, nuestra aplicación debe cargar todos los archivos de los otros tres directorios. Podemos hacerlo fácilmente ejecutando:
%w{helpers models routes}.each {|dir| Dir.glob("#{dir}/*.rb", &method(:require))}
A primera vista, esta forma de cargar puede parecer conveniente. Sin embargo, con esta única línea del código, no podemos omitir archivos fácilmente, porque cargará todos los archivos de los directorios de la matriz. Es por eso que utilizaremos un enfoque de carga de un solo archivo más eficiente, que supone que en cada carpeta tenemos un archivo de manifiesto init.rb
, que carga todos los demás archivos del directorio. Además, agregaremos un directorio de destino a la ruta de carga de Ruby:
%w{helpers models routes}.each do |dir| $LOAD_PATH << File.expand_path('.', File.join(File.dirname(__FILE__), dir)) require File.join(dir, 'init') end
Este enfoque requiere un poco más de trabajo porque tenemos que mantener las instrucciones require en cada archivo init.rb
pero, a cambio, tenemos más control y podemos omitir fácilmente uno o más archivos eliminándolos del archivo manifest init.rb
en el directorio de destino.
Autenticación de API
Lo primero que necesitamos en cada API es la autenticación. Lo implementaremos como un módulo auxiliar. La lógica de autenticación completa estará en el archivo helpers/authentication.rb
.
require 'multi_json' module Sinatra module Authentication def authenticate! client_id = request['client_id'] client_secret = request['client_secret'] # Authenticate client here halt 401, MultiJson.dump({message: "You are not authorized to access this resource"}) unless authenticated? end def current_client @current_client end def authenticated? !current_client.nil? end end helpers Authentication end
Todo lo que tenemos que hacer ahora es cargar este archivo agregando una instrucción require en el archivo de manifiesto del asistente ( helpers/init.rb
) y llamar a la authenticate!
en el gancho before
de Sinatra que se ejecutará antes de procesar cualquier solicitud.
before do authenticate! end
Base de datos
A continuación, tenemos que preparar nuestra base de datos para la aplicación. Hay muchas formas de preparar la base de datos, pero como estamos usando Sequel, es natural hacerlo usando migradores. Sequel viene con dos tipos de migradores: enteros y basados en marcas de tiempo. Cada uno tiene sus ventajas y desventajas. En nuestro ejemplo, decidimos utilizar el migrador de marca de tiempo de Sequel, que requiere que los archivos de migración tengan un prefijo de marca de tiempo. El migrador de marcas de tiempo es muy flexible y puede aceptar varios formatos de marcas de tiempo, pero solo usaremos el que consta de año, mes, día, hora, minuto y segundo. Aquí están nuestros dos archivos de migración:
# db/migrations/20160710094000_sports.rb Sequel.migration do change do create_table(:sports) do primary_key :id String :name, :null => false end end end # db/migrations/20160710094100_players.rb Sequel.migration do change do create_table(:players) do primary_key :id String :name, :null => false foreign_key :sport_id, :sports end end end
Ahora estamos listos para crear una base de datos con todas las tablas.
bundle exec sequel -m db/migrations sqlite://db/development.sqlite3
Finalmente, tenemos los archivos de modelo sport.rb
y player.rb
en el directorio de models
.
# models/sport.rb class Sport < Sequel::Model one_to_many :players def to_api { id: id, name: name } end end # models/player.rb class Player < Sequel::Model many_to_one :sport def to_api { id: id, name: name, sport_id: sport_id } end end
Aquí estamos empleando una forma de Sequel de definir las relaciones de modelo, donde el objeto Sport
tiene muchos jugadores y el Player
solo puede tener un deporte. Además, cada modelo define su método to_api
, que devuelve un hash con los atributos que deben serializarse. Este es un enfoque general que podemos usar para varios formatos. Sin embargo, si solo usamos un formato JSON en nuestra API, podríamos usar to_json
de Ruby con un only
argumento para restringir la serialización a los atributos requeridos, es decir, player.to_json(only: [:id, :name, :sport_i])
. Por supuesto, también podríamos definir un BaseModel
que herede de Sequel::Model
y defina un método to_api
predeterminado, del cual heredar todos los modelos podrían luego heredar.
Ahora, podemos comenzar a implementar los puntos finales reales de la API.
Puntos finales de la API
Mantendremos la definición de todos los puntos finales en archivos dentro del directorio de routes
. Dado que estamos utilizando archivos de manifiesto para cargar archivos, agruparemos las rutas por recursos (es decir, mantendremos todas las rutas relacionadas con los deportes en el archivo sports.rb
, todas las rutas de los jugadores en routes.rb
, etc.).
# routes/sports.rb class DemoApi < Sinatra::Application get "/sports/?" do MultiJson.dump(Sport.all.map { |s| s.to_api }) end get "/sports/:id" do sport = Sport.where(id: params[:id]).first MultiJson.dump(sport ? sport.to_api : {}) end get "/sports/:id/players/?" do sport = Sport.where(id: params[:id]).first MultiJson.dump(sport ? sport.players.map { |p| p.to_api } : []) end end # routes/players.rb class DemoApi < Sinatra::Application get "/players/?" do MultiJson.dump(Player.all.map { |p| s.to_api }) end get "/players/:id/?" do player = Player.where(id: params[:id]).first MultiJson.dump(player ? player.to_api : {}) end end
Las rutas anidadas, como la que permite obtener todos los jugadores dentro de un deporte /sports/:id/players
, se pueden definir colocándolas junto con otras rutas o creando un archivo de recursos separado que contendrá solo las rutas anidadas.
Con las rutas designadas, la aplicación ahora está lista para aceptar solicitudes:
curl -i -XGET 'http://localhost:9292/sports?client_id=<client_id>&client_secret=<client_secret>'
Tenga en cuenta que, según lo requiere el sistema de autenticación de la aplicación definido en el archivo helpers/authentication.rb
, estamos pasando las credenciales directamente en los parámetros de solicitud.
Conclusión
Los principios demostrados en esta sencilla aplicación de ejemplo se aplican a cualquier aplicación de back-end de API. No se basa en la arquitectura modelo-vista-controlador (MVC), pero mantiene una clara separación de responsabilidades de manera similar; La lógica de negocios completa se mantiene en los archivos del modelo, mientras que el manejo de las solicitudes se realiza en los métodos de rutas de Sinatra. A diferencia de la arquitectura MVC, donde las vistas se usan para generar respuestas, esta aplicación lo hace en el mismo lugar donde maneja las solicitudes: en los métodos de rutas. Con nuevos archivos auxiliares, la aplicación se puede ampliar fácilmente para enviar paginación o, si es necesario, solicitar información de límites al usuario en los encabezados de respuesta.
Al final, creamos una API completa con un conjunto de herramientas muy simple y sin perder ninguna funcionalidad. El número limitado de dependencias ayuda a garantizar que la aplicación se carga y se inicia mucho más rápido, y tiene una huella de memoria mucho menor que la que tendría una basada en Rails. Entonces, la próxima vez que comience a trabajar en una nueva API en Ruby, considere usar Sinatra y Sequel, ya que son herramientas muy poderosas para tal caso de uso.