Ruby Web APIs — Building RESTful APIs with Sinatra, Rails API, and Grape
In this tutorial, you will learn about Ruby Web APIs. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby web APIs are built with Sinatra for lightweight services, Rails API mode for full-featured APIs, and Grape for DSL-based RESTful endpoints.
What You'll Learn
- Building APIs with Sinatra
- Rails API mode
- Request validation and serialization
- Authentication and rate limiting
Why It Matters
APIs power modern applications. GitHub API is built with Ruby. Shopify API handles billions of requests. DodaZIP exposes a REST API for file processing.
Real-World Use
Mobile app backends, third-party integrations, microservices, SaaS platforms, IoT device communication.
flowchart LR
A["Web APIs"] --> B["Sinatra"]
A --> C["Rails API"]
A --> D["Grape"]
B --> E["Endpoints"]
C --> F["Serializers"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
style F fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Sinatra API
require "sinatra"
require "json"
set :port, 4567
tasks = []
before do
content_type :json
end
get "/tasks" do
tasks.to_json
end
post "/tasks" do
body = JSON.parse(request.body.read)
tasks << { id: tasks.size + 1, **body }
status 201
tasks.last.to_json
end
get "/tasks/:id" do
task = tasks.find { |t| t[:id] == params[:id].to_i }
halt 404, { error: "Not found" }.to_json unless task
task.to_json
end
delete "/tasks/:id" do
tasks.reject! { |t| t[:id] == params[:id].to_i }
status 204
end
Rails API Mode
# config/application.rb
module MyApi
class Application < Rails::Application
config.api_only = true
end
end
# app/controllers/api/v1/tasks_controller.rb
module Api
module V1
class TasksController < ApplicationController
before_action :set_task, only: [:show, :update, :destroy]
def index
tasks = Task.all
render json: TaskSerializer.new(tasks).serializable_hash
end
def show
render json: TaskSerializer.new(@task).serializable_hash
end
def create
task = Task.new(task_params)
if task.save
render json: TaskSerializer.new(task).serializable_hash, status: :created
else
render json: { errors: task.errors }, status: :unprocessable_entity
end
end
private
def set_task
@task = Task.find(params[:id])
end
def task_params
params.require(:task).permit(:title, :completed)
end
end
end
end
Grape API
require "grape"
class TaskAPI < Grape::API
format :json
helpers do
def tasks
@tasks ||= []
end
end
resource :tasks do
desc "List all tasks"
get do
tasks
end
desc "Create a task"
params do
requires :title, type: String
optional :completed, type: Boolean, default: false
end
post do
task = { id: tasks.size + 1, **declared(params) }
tasks << task
task
end
route_param :id do
desc "Get a task"
get do
tasks.find { |t| t[:id] == params[:id].to_i } || error!("Not found", 404)
end
end
end
end
Authentication
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
before_action :authenticate
def authenticate
token = request.headers["Authorization"]&.split(" ")&.last
@current_user = User.find_by(api_token: token)
render json: { error: "Unauthorized" }, status: :unauthorized unless @current_user
end
end
Rate Limiting
class RateLimiter
def initialize(app)
@app = app
@requests = {}
end
def call(env)
ip = env["REMOTE_ADDR"]
now = Time.now
@requests[ip] = @requests[ip].select { |t| now - t < 60 }
if @requests[ip].size >= 100
[429, { "Content-Type" => "application/json" },
[{ error: "Rate limit exceeded" }.to_json]]
else
@requests[ip] << now
@app.call(env)
end
end
end
Common Mistakes
1. No Input Validation
Always validate and sanitize input params. Use strong parameters in Rails or params validation in Grape.
2. No Error Handling
Return consistent error responses. Use HTTP status codes correctly (400, 401, 404, 422, 500).
3. Missing Pagination
Always paginate list endpoints. Use page and per params. Return total count and pagination metadata.
4. No Versioning
Version your API from day one (/api/v1/tasks). Breaking changes go in v2 without affecting v1 clients.
5. Exposing Internal IDs
Don't expose sequential database IDs. Use UUIDs or hashed IDs in public API responses.
Practice Questions
1. What is Rails API mode? Rails configured with config.api_only = true. Excludes views, cookies, sessions. Optimized for API performance.
2. How do you handle CORS? Use the rack-cors gem. Configure allowed origins, methods, and headers in middleware.
3. What serialization format should I use? JSON is the standard. Use ActiveModelSerializers or jsonapi-serializer for structured responses.
4. How do you document APIs? Use Rswag (Rails) or Grape::Swagger for auto-generated Swagger/OpenAPI documentation.
Challenge: Build a rate-limited API endpoint using Sinatra that tracks requests per IP.
Solution
require "sinatra"
require "json"
set :port, 4567
before { content_type :json }
requests = Hash.new { |h, k| h[k] = [] }
get "/api" do
ip = request.ip
now = Time.now
requests[ip] = requests[ip].select { |t| now - t < 60 }
if requests[ip].size >= 5
halt 429, { error: "Rate limited" }.to_json
end
requests[ip] << now
{ message: "Success", count: requests[ip].size }.to_json
end
FAQ
{{< faq question="Should I use Sinatra or Rails for APIs?" >}} Sinatra for small services and microservices. Rails API mode for full-featured APIs with database, authentication, and Background Jobs. {{< /faq >}}
{{< faq question="How do I version my API?" >}}
Use URL versioning /api/v1/ or header versioning Accept: application/vnd.myapp.v1+json. URL is simpler for clients.
{{< /faq >}}
{{< faq question="What's the best testing strategy for APIs?" >}} Use RSpec with request specs. Test happy paths, error cases, authentication, and rate limiting. Use WebMock or VCR for external API calls. {{< /faq >}}
{{< faq question="How do I handle background jobs from API calls?" >}} Offload heavy work to Sidekiq. Return 202 Accepted with a job ID. Provide a status endpoint for clients to check progress. {{< /faq >}}
{{< faq question="Should I use GraphQL or REST?" >}} REST for simple CRUD APIs. Graphql for complex data fetching with client-driven queries. Ruby gems like graphql-ruby support GraphQL well. {{< /faq >}}
Try It Yourself
require "sinatra"
require "json"
set :port, 4567
before { content_type :json }
items = []
get "/items" do
items.to_json
end
post "/items" do
data = JSON.parse(request.body.read)
items << { id: items.size + 1, name: data["name"] }
status 201
items.last.to_json
end
Expected output:
GET /items -> []
POST /items {"name":"test"} -> {"id":1,"name":"test"}
GET /items -> [{"id":1,"name":"test"}]
What's Next
Now that you understand web APIs, explore advanced Ruby topics and Metaprogramming.
| Topic | Description | Link |
|---|---|---|
| Ruby Deployment | Deploying API applications | {{< ref "48-deployment" >}} |
| Go HTTP Servers | Compare Go HTTP handling | Go |
| Ruby Metaprogramming | Dynamic Ruby techniques | {{< ref "33-metaprogramming-basics" >}} |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro