GR API Manager

Gem Version Gem Total Downloads Ruby Version Sinatra Version License: MIT Platform Tests README en Español

GR API Manager is a minimalist, high-performance Ruby wrapper on top of Sinatra and Puma, designed to build production-grade REST APIs with zero boilerplate.


System Requirements

Dependency / Environment Required / Supported Version
Ruby >= 3.0 (Tested on 3.0, 3.1, 3.2, 3.3, and 3.4)
Sinatra >= 3.0, < 5.0
Puma >= 5.0, < 9.0
Dotenv >= 2.8, < 4.0

Key Features

  • Available on RubyGems (0.4.0) - global installation or via Bundler.
  • Modular Route Groups (api.group) - organize large projects into clean, multi-file sub-routers with prefix & option inheritance.
  • Dual Authentication (Static Bearer Token & Native JWT) - support for pre-shared static tokens and zero-dependency built-in HMAC-SHA256 JWT engine.
  • Declarative Schema & Type Validation - validate complex request contracts with :email, :url, :boolean, :file, classes (Integer, Float, String, Array, Hash), enum lists, regular expressions, or custom procs.
  • Sliding-Window Rate Limiting (429) - sliding-window rate limiter with automatic client IP detection behind Cloudflare (CF-Connecting-IP), Nginx (X-Real-IP), or Reverse Proxies (X-Forwarded-For), supporting in-memory or pluggable stores (Redis).
  • Smart Parameter Casting - automatic type casting for query and URL parameters ("100" -> 100, "-42" -> -42, "true" -> true, "19.99" -> 19.99).
  • Unified File & Binary Handling (FilePayload) - transparent handling for multipart uploads, raw binary streams, Base64/Hex encoding, file serving/downloads, and disk storage with directory auto-creation.
  • Puma Concurrency - configurable multi-process workers and thread pools.
  • Developer Mode (dev_mode) - structured JSON error responses with full stack traces for painless debugging.

Installation

Add the gem to your Gemfile:

gem 'gr_api_manager', '~> 0.4.0'

And run:

bundle install

Or install directly:

gem install gr_api_manager

Table of Contents

  1. Quickstart
  2. Authentication: Static Bearer Token & Native JWT
  3. Multi-File Modular Architecture (Importing & Grouping)
  4. Full REST CRUD with HTTP Verbs
  5. Declarative Schema & Type Validation
  6. Comprehensive File, Binary & Image Handling
  7. Serving & Downloading Files to Clients
  8. Rate Limiting & Real IP Detection (Cloudflare/Nginx)
  9. Smart Parameter Casting
  10. Responses, HTTP Status Codes & Dev Mode
  11. Concurrency & Production Deployment (Puma & Docker)

Quickstart

Create an app.rb file:

require 'gr_api_manager'

# Initialize the server with static token and JWT key
api = GRApiManager::Server.new(
  port: 4000,
  bearer_token: "my_static_secret_token",
  jwt_secret: "my_jwt_signing_secret"
)

# Public endpoint (no auth required)
api.get('/health', auth: false) do
  { status: 'online', timestamp: Time.now.to_i }
end

# Protected endpoint with static token (default: auth = true)
api.get('/protected-data') do
  { message: "Access granted with Bearer Token", data: [10, 20, 30] }
end

# Start the server
api.run!

Run your API:

ruby app.rb

Authentication: Static Bearer Token & Native JWT

gr_api_manager supports two complementary authentication schemes:

1. Static Bearer Token

Ideal for internal services, webhooks, microservices, or backend automation using a pre-shared static key.

Configuration in app.rb:

api = GRApiManager::Server.new(
  bearer_token: "my_secret_company_token_2026"
)

# Public route
api.get('/public', auth: false) do
  { status: "open access" }
end

# Protected routes (default auth: true)
api.get('/admin/config') do
  { database: "connected", environment: "production" }
end

api.post('/admin/restart', requires: [:reason]) do |params|
  { action: "restarting", reason: params[:reason] }
end

Consuming via cURL:

Valid request (200 OK):

curl -H "Authorization: Bearer my_secret_company_token_2026" http://localhost:4000/admin/config
# => {"database":"connected","environment":"production"}

Missing Authorization header (401 Unauthorized):

curl http://localhost:4000/admin/config
# => 401 {"error":"Token required. Format: 'Bearer <token>'"}

Invalid token (403 Forbidden):

curl -H "Authorization: Bearer wrong_token" http://localhost:4000/admin/config
# => 403 {"error":"Invalid token"}

2. Native JWT Authentication (HS256)

Ideal for end-user facing APIs issuing dynamic tokens with expiration and claims.

Complete Workflow:

api = GRApiManager::Server.new(
  jwt_secret: "my_super_secret_jwt_key"
)

# 1. Login: issue and return token
api.post('/auth/login', auth: false, requires: { email: :email, password: String }) do |params|
  if params[:email] == "admin@company.com" && params[:password] == "pass123"
    token = api.jwt_encode(
      { user_id: 42, email: params[:email], role: "admin" },
      exp: Time.now.to_i + 3600 # 1-hour expiration
    )
    { token: token, token_type: "Bearer", expires_in: 3600 }
  else
    status 401
    { error: "Invalid credentials" }
  end
end

# 2. JWT-protected route: automatically injects params[:current_user]
api.get('/profile', auth: :jwt) do |params|
  user = params[:current_user]
  {
    message: "Valid JWT token",
    user_id: user[:user_id],
    email: user[:email],
    role: user[:role]
  }
end

Multi-File Modular Architecture

Divide your API into dedicated, clean route modules within a routes/ directory:

my_api_project/
├── app.rb                   # Main entrypoint and configuration
├── .env                     # Environment variables
├── Gemfile
└── routes/
    ├── auth_routes.rb       # Login & registration
    ├── admin_routes.rb      # Admin dashboard
    ├── payments_routes.rb   # Checkout & billing
    └── files_routes.rb      # File uploads & storage

Module 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] == "admin@company.com" && params[:password] == "secret"
        token = api_server.jwt_encode({ user_id: 1, email: params[:email], role: "admin" })
        { token: token }
      else
        status 401
        { error: "Invalid credentials" }
      end
    end
  end
end

Module 2: routes/admin_routes.rb

module AdminRoutes
  def self.setup(router)
    router.get('/metrics') do |params|
      { cpu: "12%", memory: "380MB", user: params[:current_user][:email] }
    end

    router.delete('/users/:id') do |params|
      { message: "User #{params[:id]} deleted" }
    end
  end
end

Module 3: routes/files_routes.rb

module FilesRoutes
  def self.setup(router)
    router.post('/upload', requires: [:title]) do |params|
      file = params[:_files][:document]
      path = file.save_to("./storage/#{params[:title]}#{file.extension}")
      { status: "saved", path: path, size: file.size }
    end
  end
end

Main Application File: app.rb

require 'gr_api_manager'

# Require modular route files
require_relative 'routes/auth_routes'
require_relative 'routes/admin_routes'
require_relative 'routes/files_routes'

api = GRApiManager::Server.new(
  port: 4000,
  jwt_secret: ENV['JWT_SECRET'] || "default_jwt_secret",
  bearer_token: ENV['API_TOKEN'] || "global_static_token"
)

# Root route
api.get('/', auth: false) { { service: "Core API v1.0" } }

# Mount route groups
api.group('/auth') { |g| AuthRoutes.setup(g, api) }
api.group('/admin', auth: :jwt) { |g| AdminRoutes.setup(g) }
api.group('/files', auth: true) { |g| FilesRoutes.setup(g) }

api.run!(workers: 2, threads: '2:8')

Full REST CRUD with HTTP Verbs

Example managing a /products resource:

api = GRApiManager::Server.new(prefix: '/api/v1')

# 1. LIST (GET) - with auto-cast query parameters
api.get('/products', auth: false) do |params|
  page   = params[:page] || 1        # Integer
  limit  = params[:limit] || 10      # Integer
  active = params[:active] != false  # Boolean
  
  {
    page: page,
    limit: limit,
    items: [
      { id: 1, name: "Mechanical Keyboard", price: 89.99, active: true },
      { id: 2, name: "4K Monitor", price: 299.99, active: true }
    ]
  }
end

# 2. GET BY ID (GET)
api.get('/products/:id', auth: false) do |params|
  id = params[:id] # Auto Integer
  { id: id, name: "Product #{id}", price: 49.99 }
end

# 3. CREATE (POST) - with strong type validation
api.post('/products', requires: { name: String, price: Float, category: ['tech', 'office'] }) do |params|
  status 201
  {
    message: "Product created",
    product: { id: rand(100..999), name: params[:name], price: params[:price] }
  }
end

# 4. FULL UPDATE (PUT)
api.put('/products/:id', requires: { name: String, price: Float }) do |params|
  {
    message: "Product #{params[:id]} completely updated",
    data: params
  }
end

# 5. PARTIAL UPDATE (PATCH)
api.patch('/products/:id') do |params|
  {
    message: "Updated fields for product #{params[:id]}",
    changes: params.except(:id)
  }
end

# 6. DELETE (DELETE)
api.delete('/products/:id') do |params|
  { message: "Product #{params[:id]} deleted successfully" }
end

Declarative Schema & Type Validation

The requires: option validates data types, string formats, uploaded files, and custom logic:

api.post '/catalog', requires: {
  sku:         /^[A-Z]{3}-\d{4}$/,           # Regexp: e.g. "PRO-1234"
  title:       String,                       # Non-empty string
  price:       Float,                        # Float number
  stock:       Integer,                      # Integer
  active:      :boolean,                     # true or false
  category:    ['electronics', 'home'],      # Enum / List of allowed values
  photo:       :file,                        # Uploaded FilePayload
  website:     :url,                         # Valid URL (http/https)
  contact:     :email,                       # Valid email address
  discount:    ->(v) { v.to_f.between?(0, 100) } # Custom Lambda validation
} do |params|
  status 201
  { status: "ok", item: params[:title] }
end

Supported Validation Rules:

Rule Expected Format / Type Valid Example
:email Standard email format "contact@domain.com"
:url URL with http:// or https:// "https://api.domain.com"
:boolean Native boolean (true or false) true, false
:file Instance of GRApiManager::FilePayload Multipart file upload
Integer Integer number 42, 100, -10
Float Floating point number 19.99, 0.5, -3.14
Numeric Any numeric value (Integer or Float) 10, 3.14
String Non-empty string "Sample Text"
Array Array of elements [1, 2, 3]
Hash JSON Object or Hash { key: "value" }
['a', 'b'] Exact inclusion in list (Enum) 'electronics'
/^regex$/ Regular expression match "ABC-1234"
->(val) { ... } Custom Lambda/Proc (must return true) ->(n) { n.to_i > 0 }

Comprehensive File, Binary & Image Handling

gr_api_manager detects incoming Content-Type headers and provides uniform file handling via FilePayload:

1. Multipart Form Upload (multipart/form-data)

api.post('/profile/avatar', requires: [:user_id]) do |params|
  avatar = params[:_files][:avatar] # FilePayload instance

  # Save to disk (creates directories automatically if missing)
  saved_path = avatar.save_to("./storage/avatars/user_#{params[:user_id]}#{avatar.extension}")

  {
    message: "Avatar saved",
    filename: avatar.filename,
    size: avatar.size,
    extension: avatar.extension,
    saved_path: saved_path
  }
end

cURL multipart:

curl -X POST http://localhost:4000/profile/avatar \
  -H "Authorization: Bearer my_token" \
  -F "user_id=10" \
  -F "avatar=@/path/to/my_photo.jpg"

2. Raw Binary Upload (Image / PDF / Octet-Stream)

Direct raw byte streaming in request body (no multipart form):

api.post('/documents/raw') do |params|
  file = params[:_raw_binary] # FilePayload instance

  file.save_to("./storage/docs/#{file.filename}")
  
  {
    format: "raw binary",
    detected_filename: file.filename,
    size_bytes: file.size,
    mime_type: file.content_type,
    initial_hex: file.to_hex[0..30]
  }
end

cURL raw binary:

curl -X POST http://localhost:4000/documents/raw \
  -H "Authorization: Bearer my_token" \
  -H "Content-Type: application/pdf" \
  -H "Content-Disposition: attachment; filename=\"contract.pdf\"" \
  --data-binary @contract.pdf

3. Base64 and Hexadecimal Conversion

api.post('/files/convert') do |params|
  file = params[:_files][:file]

  {
    base64: file.to_base64, # Clean Base64 string without newlines
    hexadecimal: file.to_hex, # Lowercase hex string
    total_bytes: file.size
  }
end

4. Plain Text Upload (text/plain)

api.post('/logs/text') do |params|
  raw_text = params[:_raw_text] # UTF-8 String
  { lines: raw_text.lines.count, characters: raw_text.length }
end

Serving & Downloading Files to Clients

When a route block returns a String, gr_api_manager serves it directly as raw data, allowing seamless downloads of images, PDFs, or binary streams:

api.get('/downloads/photo/:id', auth: false) do |params|
  photo_path = "./storage/avatars/user_#{params[:id]}.jpg"
  
  unless File.exist?(photo_path)
    status 404
    next { error: "Photo not found" }
  end

  # Configure response headers
  content_type 'image/jpeg'
  headers 'Content-Disposition' => "inline; filename=\"photo_#{params[:id]}.jpg\""
  
  # Return binary bytes directly
  File.binread(photo_path)
end

Rate Limiting & Real IP Detection

Protect your API with thread-safe sliding-window rate limiting per client IP:

api = GRApiManager::Server.new(
  rate_limit:          60,   # Max 60 requests
  rate_limit_window:   60,   # per 60-second window
  trust_proxy_headers: true  # Reads CF-Connecting-IP, X-Real-IP, X-Forwarded-For
)

Standard HTTP response headers:

  • X-RateLimit-Limit: Maximum allowed requests (60).
  • X-RateLimit-Remaining: Remaining requests in current window.
  • X-RateLimit-Reset: Unix timestamp when quota resets.
  • Retry-After: Seconds to wait if rate-limited (429 Too Many Requests).

Smart Parameter Casting

URL and Query String parameters are automatically converted to native Ruby types before entering your route block:

api.get('/analytics') do |params|
  # Request: /analytics?id=123&active=true&discount=15.5&balance=-500&category=tech
  
  params[:id]        # => 123 (Integer)
  params[:active]    # => true (TrueClass)
  params[:discount] # => 15.5 (Float)
  params[:balance]   # => -500 (Integer)
  params[:category]  # => "tech" (String)
  
  { status: "ok" }
end

Responses, HTTP Status Codes & Dev Mode

Custom status codes:

api.post('/resources') do
  status 201 # Created
  { message: "Resource created" }
end

Developer Mode (dev_mode: true):

Enable dev_mode: true to receive structured JSON error payloads with full stack traces during development:

api = GRApiManager::Server.new(dev_mode: true)

Error response on 500:

{
  "error": "Internal Server Error",
  "details": "undefined local variable or method 'missing_var'",
  "class": "NameError",
  "backtrace": [
    "/app/routes/users.rb:14:in `block in setup'",
    "/lib/gr_api_manager.rb:482:in `instance_exec'"
  ]
}

Concurrency & Production Deployment

Running with Puma in Production:

# Start with 4 worker processes and 4:16 threads per worker
api.run!(workers: 4, threads: '4:16')

Production Dockerfile:

FROM ruby:3.3-slim

WORKDIR /app
COPY Gemfile* ./
RUN bundle install --without development test

COPY . .

EXPOSE 4000
CMD ["ruby", "app.rb"]

Automated Testing

Full test suite with RSpec and Rack::Test:

rspec
# => 59 examples, 0 failures

License

This project is licensed under the MIT License. Created by Gabo Razo.