Skip to content

Ruby Web APIs — Building RESTful APIs with Sinatra, Rails API, and Grape

DodaTech Updated 2026-06-28 5 min read

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