GR API Manager
GR API Manager es un micro-framework y wrapper de alto rendimiento de Ruby sobre Sinatra y Puma, diseñado para construir APIs REST profesionales sin código repetitivo (zero boilerplate).
Requisitos del Sistema
| Dependencia / Entorno | Version requerida / soportada |
|---|---|
| Ruby | >= 3.0 (Probado en 3.0, 3.1, 3.2, 3.3 y 3.4) |
| Sinatra | >= 3.0, < 5.0 |
| Puma | >= 5.0, < 9.0 |
| Dotenv | >= 2.8, < 4.0 |
Caracteristicas Principales
- Disponible en RubyGems (
0.4.0) - instalacion global o via Bundler. - Grupos de Rutas Modulares (
api.group) - organiza proyectos grandes en multiples archivos y modulos independientes con herencia de prefijos y opciones. - Autenticacion Dual (Bearer Token Fijo & JWT Nativo) - soporte para tokens estaticos pre-compartidos y motor JWT (HMAC-SHA256) integrado sin gemas externas.
- Validacion Declarativa de Esquemas y Tipos - valida contratos de datos complejos con
:email,:url,:boolean,:file, clases (Integer,Float,String,Array,Hash), listas enum, expresiones regulares o lambdas. - Control de Tasa (Rate Limiting 429) - algoritmo sliding window con deteccion automatica de IP real tras Cloudflare (
CF-Connecting-IP), Nginx (X-Real-IP) o Proxies (X-Forwarded-For), con soporte para almacenes en memoria o externos (Redis). - Cast Inteligente de Tipos - conversion automatica de parametros de query/URL (
"100"->100,"-42"->-42,"true"->true,"19.99"->19.99). - Manejo Integral de Archivos y Binarios (
FilePayload) - soporte transparente para uploads multipart, flujos binarios crudos, conversion a Base64, Hexadecimal, descarga de archivos y guardado automatico en disco. - Servidor Concurrente Puma - soporte para multiples procesos worker y pools de threads.
- Modo de Depuracion (
dev_mode) - errores 500 estructurados en JSON con stack trace detallado para desarrollo.
Instalacion
Agrega la gema a tu Gemfile:
gem 'gr_api_manager', '~> 0.4.0'
Y ejecuta:
bundle install
O instalala directamente en tu sistema:
gem install gr_api_manager
Tabla de Contenidos
- Inicio Rapido
- Autenticacion: Token Fijo (Bearer) y JWT Nativo
- Estructura Modular Multi-Archivo (Importar y Agrupar)
- CRUD Completo con Verbos HTTP
- Validacion Declarativa de Esquemas y Tipos
- Manejo Integral de Archivos, Binarios e Imagenes
- Descarga y Servir Archivos al Cliente
- Rate Limiting y Deteccion de IP Real (Cloudflare/Nginx)
- Cast Inteligente de Parametros
- Respuestas, Codigos HTTP y Modo Desarrollo
- Concurrencia y Produccion (Puma & Docker)
Inicio Rapido
Crea un archivo app.rb:
require 'gr_api_manager'
# Inicializa el servidor con un token fijo y clave JWT
api = GRApiManager::Server.new(
port: 4000,
bearer_token: "mi_token_fijo_secreto",
jwt_secret: "mi_firma_jwt"
)
# Endpoint publico (sin autenticacion)
api.get('/health', auth: false) do
{ status: 'online', timestamp: Time.now.to_i }
end
# Endpoint protegido con token fijo (default: auth = true)
api.get('/datos-protegidos') do
{ mensaje: "Acceso autorizado con Bearer Token", datos: [10, 20, 30] }
end
# Inicia el servidor
api.run!
Ejecuta tu API:
ruby app.rb
Autenticacion: Token Fijo (Bearer) y JWT Nativo
gr_api_manager soporta dos esquemas de autenticacion complementarios:
1. Token Fijo (Static Bearer Token)
Ideal para APIs privadas, comunicacion entre microservicios, webhooks o scripts backend donde existe una clave fija pre-compartida.
Configuracion en app.rb:
api = GRApiManager::Server.new(
bearer_token: "clave_secreta_empresa_2026"
)
# Ruta publica
api.get('/publico', auth: false) do
{ estado: "acceso libre" }
end
# Rutas protegidas (por defecto auth: true)
api.get('/admin/config') do
{ base_datos: "conectada", entorno: "produccion" }
end
api.post('/admin/reiniciar', requires: [:motivo]) do |params|
{ accion: "reiniciando", motivo: params[:motivo] }
end
Como consumir desde cURL o clientes HTTP:
Peticion valida (200 OK):
curl -H "Authorization: Bearer clave_secreta_empresa_2026" http://localhost:4000/admin/config
# => {"base_datos":"conectada","entorno":"produccion"}
Peticion sin cabecera (401 Unauthorized):
curl http://localhost:4000/admin/config
# => 401 {"error":"Token required. Format: 'Bearer <token>'"}
Peticion con token invalido (403 Forbidden):
curl -H "Authorization: Bearer token_falso" http://localhost:4000/admin/config
# => 403 {"error":"Invalid token"}
2. Autenticacion JWT Nativa (HS256)
Ideal para APIs con usuarios finales donde se emiten tokens dinamicos con roles y tiempo de expiracion.
Configuracion y Flujo Completo:
api = GRApiManager::Server.new(
jwt_secret: "clave_secreta_para_firmar_jwts"
)
# 1. Login: genera y entrega el token al usuario
api.post('/auth/login', auth: false, requires: { email: :email, password: String }) do |params|
# Validar credenciales contra base de datos
if params[:email] == "[email protected]" && params[:password] == "pass123"
token = api.jwt_encode(
{ user_id: 42, email: params[:email], role: "admin" },
exp: Time.now.to_i + 3600 # Expira en 1 hora
)
{ token: token, token_type: "Bearer", expira_en: 3600 }
else
status 401
{ error: "Credenciales invalidas" }
end
end
# 2. Ruta protegida por JWT: inyecta automaticamente params[:current_user]
api.get('/perfil', auth: :jwt) do |params|
usuario = params[:current_user]
{
mensaje: "Token JWT valido",
id_usuario: usuario[:user_id],
email: usuario[:email],
rol: usuario[:role]
}
end
Estructura Modular Multi-Archivo
Divide tu API en modulos independientes y ordenados dentro de una carpeta routes/:
mi_proyecto/
├── app.rb # Archivo principal de configuracion y arranque
├── .env # Variables de entorno
├── Gemfile
└── routes/
├── auth_routes.rb # Login y registro
├── admin_routes.rb # Panel de administracion
├── pagos_routes.rb # Pasarela de pagos
└── archivos_routes.rb # Subida y descarga de archivos
Modulo 1: routes/auth_routes.rb
module AuthRoutes
def self.setup(router, api_server)
router.post('/login', auth: false, requires: { email: :email, password: String }) do |params|
if params[:email] == "[email protected]" && params[:password] == "secreto"
token = api_server.jwt_encode({ user_id: 1, email: params[:email], role: "admin" })
{ token: token }
else
status 401
{ error: "Credenciales incorrectas" }
end
end
end
end
Modulo 2: routes/admin_routes.rb
module AdminRoutes
def self.setup(router)
router.get('/metricas') do |params|
{ cpu: "12%", memoria: "380MB", usuario: params[:current_user][:email] }
end
router.delete('/usuarios/:id') do |params|
{ mensaje: "Usuario #{params[:id]} eliminado" }
end
end
end
Modulo 3: routes/archivos_routes.rb
module ArchivosRoutes
def self.setup(router)
router.post('/subir', requires: [:nombre]) do |params|
archivo = params[:_files][:documento]
ruta = archivo.save_to("./almacen/#{params[:nombre]}#{archivo.extension}")
{ status: "guardado", ruta: ruta, tamano: archivo.size }
end
end
end
Archivo Principal: app.rb
require 'gr_api_manager'
# Importar los modulos de rutas
require_relative 'routes/auth_routes'
require_relative 'routes/admin_routes'
require_relative 'routes/archivos_routes'
api = GRApiManager::Server.new(
port: 4000,
jwt_secret: ENV['JWT_SECRET'] || "clave_jwt_por_defecto",
bearer_token: ENV['API_TOKEN'] || "token_fijo_global"
)
# Ruta raiz
api.get('/', auth: false) { { servicio: "API Central v1.0" } }
# Montar los grupos de rutas
api.group('/auth') { |g| AuthRoutes.setup(g, api) }
api.group('/admin', auth: :jwt) { |g| AdminRoutes.setup(g) }
api.group('/archivos', auth: true) { |g| ArchivosRoutes.setup(g) }
api.run!(workers: 2, threads: '2:8')
CRUD Completo con Verbos HTTP
Ejemplo de gestion completa de un recurso /productos:
api = GRApiManager::Server.new(prefix: '/api/v1')
# 1. LISTAR (GET) - con parametros query auto-casteados
api.get('/productos', auth: false) do |params|
pagina = params[:pagina] || 1 # Integer
limite = params[:limite] || 10 # Integer
activo = params[:activo] != false # Boolean
{
pagina: pagina,
limite: limite,
items: [
{ id: 1, nombre: "Teclado Mecanico", precio: 89.99, activo: true },
{ id: 2, nombre: "Monitor 4K", precio: 299.99, activo: true }
]
}
end
# 2. OBTENER POR ID (GET)
api.get('/productos/:id', auth: false) do |params|
id = params[:id] # Integer automatico
{ id: id, nombre: "Producto #{id}", precio: 49.99 }
end
# 3. CREAR (POST) - con validacion de tipos
api.post('/productos', requires: { nombre: String, precio: Float, categoria: ['tech', 'oficina'] }) do |params|
status 201
{
mensaje: "Producto creado",
producto: { id: rand(100..999), nombre: params[:nombre], precio: params[:precio] }
}
end
# 4. REEMPLAZAR COMPLETO (PUT)
api.put('/productos/:id', requires: { nombre: String, precio: Float }) do |params|
{
mensaje: "Producto #{params[:id]} actualizado por completo",
datos: params
}
end
# 5. ACTUALIZACION PARCIAL (PATCH)
api.patch('/productos/:id') do |params|
{
mensaje: "Campos modificados en producto #{params[:id]}",
cambios: params.except(:id)
}
end
# 6. ELIMINAR (DELETE)
api.delete('/productos/:id') do |params|
{ mensaje: "Producto #{params[:id]} eliminado con exito" }
end
Validacion Declarativa de Esquemas y Tipos
La opcion requires: permite validar tipos de datos, formatos de texto, archivos y reglas personalizadas:
api.post '/catalogo', requires: {
codigo: /^[A-Z]{3}-\d{4}$/, # Regex: ej. "PRO-1234"
titulo: String, # Cadena no vacia
precio: Float, # Numero decimal
stock: Integer, # Numero entero
activo: :boolean, # true o false
categoria: ['electronica', 'hogar'], # Enum / Lista de opciones
foto: :file, # Archivo subido (FilePayload)
web_fab: :url, # URL valida (http/https)
contacto: :email, # Correo electronico valido
descuento: ->(v) { v.to_f.between?(0, 100) } # Lambda personalizada
} do |params|
status 201
{ status: "ok", item: params[:titulo] }
end
Tabla de Reglas de Validacion:
| Regla | Tipo / Formato | Ejemplo valido |
|---|---|---|
:email |
Correo electronico estandar | "[email protected]" |
:url |
URL con protocolo http:// o https:// |
"https://api.empresa.com" |
:boolean |
Booleano nativo (true o false) |
true, false |
:file |
Instancia de GRApiManager::FilePayload |
Archivo subido via multipart |
Integer |
Numero entero | 42, 100, -10 |
Float |
Numero de coma flotante | 19.99, 0.5, -3.14 |
Numeric |
Cualquier numero (Integer o Float) |
10, 3.14 |
String |
Cadena de texto no vacia | "Texto" |
Array |
Arreglo de elementos | [1, 2, 3] |
Hash |
Objeto o diccionario JSON | { clave: "valor" } |
['a', 'b'] |
Inclusion obligatoria en lista (Enum) | 'electronica' |
/^regex$/ |
Expresion regular | "ABC-1234" |
->(val) { ... } |
Funcion / Lambda (debe retornar true) |
->(n) { n.to_i > 0 } |
Manejo Integral de Archivos, Binarios e Imagenes
gr_api_manager detecta automaticamente el Content-Type de la peticion y unifica el acceso mediante la clase FilePayload:
1. Subida Multipart (multipart/form-data)
api.post('/perfil/avatar', requires: [:usuario_id]) do |params|
avatar = params[:_files][:avatar] # FilePayload
# Guardar en disco (crea carpetas intermedias automaticamente)
ruta = avatar.save_to("./almacen/avatares/user_#{params[:usuario_id]}#{avatar.extension}")
{
mensaje: "Avatar guardado",
archivo: avatar.filename,
tamano: avatar.size,
extension: avatar.extension,
guardado_en: ruta
}
end
cURL multipart:
curl -X POST http://localhost:4000/perfil/avatar \
-H "Authorization: Bearer mi_token" \
-F "usuario_id=10" \
-F "avatar=@/ruta/a/mi_foto.jpg"
2. Subida de Binario Crudo (Raw Binary / Image / PDF Stream)
Envio directo de bytes en el body de la peticion (sin multipart):
api.post('/documentos/raw') do |params|
archivo = params[:_raw_binary] # FilePayload
archivo.save_to("./almacen/docs/#{archivo.filename}")
{
formato: "binario crudo",
nombre_detectado: archivo.filename,
tamano_bytes: archivo.size,
mime_type: archivo.content_type,
hex_inicial: archivo.to_hex[0..30]
}
end
cURL binario crudo:
curl -X POST http://localhost:4000/documentos/raw \
-H "Authorization: Bearer mi_token" \
-H "Content-Type: application/pdf" \
-H "Content-Disposition: attachment; filename=\"contrato.pdf\"" \
--data-binary @contrato.pdf
3. Conversion a Base64 y Hexadecimal
api.post('/archivos/convertir') do |params|
archivo = params[:_files][:archivo]
{
base64: archivo.to_base64, # Cadena Base64 limpia (sin saltos de linea)
hexadecimal: archivo.to_hex, # Cadena Hexadecimal en minusculas
bytes_totales: archivo.size
}
end
4. Subida en Texto Plano (text/plain)
api.post('/logs/texto') do |params|
texto_crudo = params[:_raw_text] # String UTF-8
{ lineas: texto_crudo.lines.count, caracteres: texto_crudo.length }
end
Descarga y Servir Archivos al Cliente
Si retornas un String desde el bloque de la ruta, gr_api_manager lo entrega directamente como flujo de datos, permitiendo servir imagenes, PDFs o descargas binarias:
api.get('/descargas/foto/:id', auth: false) do |params|
ruta_foto = "./almacen/avatares/user_#{params[:id]}.jpg"
unless File.exist?(ruta_foto)
status 404
next { error: "Foto no encontrada" }
end
# Configurar cabeceras de respuesta HTTP
content_type 'image/jpeg'
headers 'Content-Disposition' => "inline; filename=\"foto_#{params[:id]}.jpg\""
# Retornar los bytes del archivo directamente
File.binread(ruta_foto)
end
Rate Limiting y Deteccion de IP Real
Protege tu API con control de tasa deslizante (sliding-window) por IP de cliente:
api = GRApiManager::Server.new(
rate_limit: 60, # Maximo 60 peticiones
rate_limit_window: 60, # por cada ventana de 60 segundos
trust_proxy_headers: true # Lee CF-Connecting-IP, X-Real-IP, X-Forwarded-For
)
Cabeceras HTTP devueltas en cada peticion:
X-RateLimit-Limit: Limite maximo permitido (60).X-RateLimit-Remaining: Peticiones restantes en la ventana actual.X-RateLimit-Reset: Timestamp Unix cuando se reinicia la cuota.Retry-After: Segundos a esperar si se excede el limite (429 Too Many Requests).
Cast Inteligente de Parametros
Los parametros de URL y Query String se transforman automaticamente a tipos nativos de Ruby:
api.get('/analisis') do |params|
# Peticion: /analisis?id=123&activo=true&descuento=15.5&saldo=-500&categoria=tech
params[:id] # => 123 (Integer)
params[:activo] # => true (TrueClass)
params[:descuento] # => 15.5 (Float)
params[:saldo] # => -500 (Integer)
params[:categoria] # => "tech" (String)
{ status: "ok" }
end
Respuestas, Codigos HTTP y Modo Desarrollo
Codigos de estado personalizados:
api.post('/recursos') do
status 201 # Created
{ mensaje: "Recurso creado" }
end
Modo Desarrollo (dev_mode: true):
En desarrollo, activa dev_mode: true para obtener detalles exactos y stack traces en JSON al ocurrir un error inesperado (500):
api = GRApiManager::Server.new(dev_mode: true)
Respuesta en 500:
{
"error": "Internal Server Error",
"details": "undefined local variable or method 'variable_inexistente'",
"class": "NameError",
"backtrace": [
"/app/routes/usuarios.rb:14:in `block in setup'",
"/lib/gr_api_manager.rb:482:in `instance_exec'"
]
}
Concurrencia y Produccion
Ejecutar con Puma en Produccion:
# Inicia con 4 procesos worker y entre 4 y 16 threads por worker
api.run!(workers: 4, threads: '4:16')
Dockerfile de Produccion:
FROM ruby:3.3-slim
WORKDIR /app
COPY Gemfile* ./
RUN bundle install --without development test
COPY . .
EXPOSE 4000
CMD ["ruby", "app.rb"]
Pruebas Automatizadas
El framework incluye una suite completa con RSpec y Rack::Test:
rspec
# => 59 examples, 0 failures
Licencia
Este proyecto esta bajo la licencia MIT. Creado por Gabo Razo.