Ruby Routing

Building Web Applications with Rails and Sinatra

2026-02-01

Explanation

Web Routing in Ruby

Ruby has two main web frameworks: Rails (full-featured) and Sinatra (minimal). Both excel at different things.

Key Concepts

  • Routes: Map URLs to controller actions
  • RESTful: Resource-oriented routing
  • Middleware: Request/response pipeline
  • Helpers: URL generation methods

Rails vs Sinatra

| Feature | Rails | Sinatra | |---------|-------|---------| | Philosophy | Convention over config | Minimal | | Size | Full framework | Micro framework | | Learning | Steeper curve | Quick start | | Best for | Large apps | APIs, small apps |


Demonstration

Example 1: Sinatra Basics

# app.rb
require 'sinatra'
require 'sinatra/json'

# Simple routes
get '/' do
  'Hello, World!'
end

# JSON response
get '/api/status' do
  json status: 'ok', time: Time.now
end

# Route parameters
get '/users/:id' do
  user_id = params[:id]
  json id: user_id, name: "User #{user_id}"
end

# Multiple parameters
get '/posts/:year/:month/:day' do
  json(
    year: params[:year],
    month: params[:month],
    day: params[:day]
  )
end

# Query parameters
get '/search' do
  query = params[:q] || '*'
  limit = params[:limit]&.to_i || 10

  json query: query, limit: limit
end

# POST with JSON body
post '/users' do
  data = JSON.parse(request.body.read)

  unless data['name'] && data['email']
    halt 400, json(error: 'Name and email required')
  end

  user = { id: rand(1000), name: data['name'], email: data['email'] }
  status 201
  json user
end

# PUT update
put '/users/:id' do
  user_id = params[:id]
  data = JSON.parse(request.body.read)

  json id: user_id, **data
end

# DELETE
delete '/users/:id' do
  status 204
end

# Before filter (middleware)
before do
  content_type :json
end

# Error handling
error 404 do
  json error: 'Not found'
end

error 500 do
  json error: 'Internal server error'
end

Example 2: Sinatra Modular Style

# app.rb
require 'sinatra/base'
require 'sinatra/json'

class UserAPI < Sinatra::Base
  helpers Sinatra::JSON

  configure do
    set :users, {}
    set :next_id, 1
  end

  before do
    content_type :json
  end

  # Authentication middleware
  before '/api/*' do
    auth = request.env['HTTP_AUTHORIZATION']
    halt 401, json(error: 'Unauthorized') unless auth == 'Bearer secret'
  end

  get '/api/users' do
    json data: settings.users.values
  end

  get '/api/users/:id' do
    user = settings.users[params[:id].to_i]
    halt 404, json(error: 'Not found') unless user
    json data: user
  end

  post '/api/users' do
    data = JSON.parse(request.body.read)

    user = {
      id: settings.next_id,
      name: data['name'],
      email: data['email'],
      created_at: Time.now.iso8601
    }

    settings.users[user[:id]] = user
    settings.next_id += 1

    status 201
    json data: user
  end

  delete '/api/users/:id' do
    id = params[:id].to_i
    halt 404, json(error: 'Not found') unless settings.users.key?(id)

    settings.users.delete(id)
    status 204
  end
end

# Run with: ruby app.rb
UserAPI.run! if __FILE__ == $0

Example 3: Rails Routes

# config/routes.rb
Rails.application.routes.draw do
  # Root route
  root 'home#index'

  # Basic routes
  get 'about', to: 'pages#about'
  get 'contact', to: 'pages#contact'

  # RESTful resources
  resources :users do
    # Nested resources
    resources :posts, only: [:index, :create]
  end

  # Generates:
  # GET    /users          users#index
  # GET    /users/new      users#new
  # POST   /users          users#create
  # GET    /users/:id      users#show
  # GET    /users/:id/edit users#edit
  # PATCH  /users/:id      users#update
  # DELETE /users/:id      users#destroy
  # GET    /users/:user_id/posts     posts#index
  # POST   /users/:user_id/posts     posts#create

  # API namespace
  namespace :api do
    namespace :v1 do
      resources :users, only: [:index, :show, :create, :update, :destroy]
      resources :posts do
        resources :comments, shallow: true
      end
    end
  end

  # Custom routes
  get 'users/:id/profile', to: 'users#profile', as: :user_profile
  post 'users/:id/activate', to: 'users#activate'

  # Member and collection routes
  resources :articles do
    member do
      post :publish
      post :archive
    end
    collection do
      get :search
      get :drafts
    end
  end

  # Constraints
  constraints(id: /\d+/) do
    resources :products
  end

  # Subdomain routing
  constraints subdomain: 'api' do
    scope module: 'api' do
      resources :users
    end
  end
end

Example 4: Rails Controllers

# app/controllers/api/v1/users_controller.rb
module Api
  module V1
    class UsersController < ApplicationController
      before_action :set_user, only: [:show, :update, :destroy]
      before_action :authenticate_request

      # GET /api/v1/users
      def index
        @users = User.all

        # Pagination
        @users = @users.page(params[:page]).per(params[:per_page] || 10)

        # Filtering
        @users = @users.where(role: params[:role]) if params[:role]

        render json: {
          data: @users,
          meta: {
            page: @users.current_page,
            total: @users.total_count,
            total_pages: @users.total_pages
          }
        }
      end

      # GET /api/v1/users/:id
      def show
        render json: { data: @user }
      end

      # POST /api/v1/users
      def create
        @user = User.new(user_params)

        if @user.save
          render json: { data: @user }, status: :created
        else
          render json: { errors: @user.errors }, status: :unprocessable_entity
        end
      end

      # PATCH/PUT /api/v1/users/:id
      def update
        if @user.update(user_params)
          render json: { data: @user }
        else
          render json: { errors: @user.errors }, status: :unprocessable_entity
        end
      end

      # DELETE /api/v1/users/:id
      def destroy
        @user.destroy
        head :no_content
      end

      private

      def set_user
        @user = User.find(params[:id])
      rescue ActiveRecord::RecordNotFound
        render json: { error: 'User not found' }, status: :not_found
      end

      def user_params
        params.require(:user).permit(:name, :email, :role)
      end

      def authenticate_request
        token = request.headers['Authorization']&.split(' ')&.last
        render json: { error: 'Unauthorized' }, status: :unauthorized unless valid_token?(token)
      end

      def valid_token?(token)
        # Token validation logic
        token == 'valid-token'
      end
    end
  end
end

Example 5: Middleware and Filters

# Sinatra middleware
class AuthMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    request = Rack::Request.new(env)

    # Skip auth for public routes
    if public_route?(request.path)
      return @app.call(env)
    end

    token = env['HTTP_AUTHORIZATION']&.split(' ')&.last

    unless valid_token?(token)
      return [401, { 'Content-Type' => 'application/json' },
              ['{"error":"Unauthorized"}']]
    end

    # Add user to request
    env['current_user'] = decode_token(token)

    @app.call(env)
  end

  private

  def public_route?(path)
    ['/health', '/login', '/register'].include?(path)
  end

  def valid_token?(token)
    token && token.length > 10
  end

  def decode_token(token)
    { id: 1, email: 'user@example.com' }
  end
end

# Use in Sinatra
class App < Sinatra::Base
  use AuthMiddleware

  get '/profile' do
    user = request.env['current_user']
    json user: user
  end
end

# Rails concerns (shared behavior)
# app/controllers/concerns/pagination.rb
module Pagination
  extend ActiveSupport::Concern

  def paginate(collection)
    page = params[:page] || 1
    per_page = params[:per_page] || 10

    collection.page(page).per(per_page)
  end

  def pagination_meta(collection)
    {
      page: collection.current_page,
      per_page: collection.limit_value,
      total: collection.total_count,
      total_pages: collection.total_pages
    }
  end
end

# Use in controller
class PostsController < ApplicationController
  include Pagination

  def index
    @posts = paginate(Post.published)
    render json: { data: @posts, meta: pagination_meta(@posts) }
  end
end

Key Takeaways:

  • Sinatra is great for small apps and APIs
  • Rails provides powerful conventions
  • RESTful routes map to CRUD operations
  • Use namespaces for API versioning
  • Middleware handles cross-cutting concerns

Imitation

Challenge 1: Add Search Route

Task: Add a search endpoint with multiple filters.

Solution

# Sinatra
get '/api/users/search' do
  users = settings.users.values

  # Filter by name
  if params[:name]
    users = users.select { |u| u[:name].downcase.include?(params[:name].downcase) }
  end

  # Filter by role
  if params[:role]
    users = users.select { |u| u[:role] == params[:role] }
  end

  # Sort
  if params[:sort]
    field = params[:sort].to_sym
    users = users.sort_by { |u| u[field] || '' }
    users = users.reverse if params[:order] == 'desc'
  end

  # Pagination
  page = (params[:page] || 1).to_i
  per_page = (params[:per_page] || 10).to_i
  offset = (page - 1) * per_page

  json(
    data: users[offset, per_page],
    meta: {
      page: page,
      per_page: per_page,
      total: users.length
    }
  )
end

Challenge 2: Rate Limiting Middleware

Task: Implement rate limiting for API endpoints.

Solution

class RateLimiter
  def initialize(app, options = {})
    @app = app
    @limit = options[:limit] || 100
    @window = options[:window] || 3600  # 1 hour
    @requests = {}
  end

  def call(env)
    ip = env['REMOTE_ADDR']
    now = Time.now.to_i

    clean_old_requests(ip, now)

    if rate_limited?(ip)
      return [429, {
        'Content-Type' => 'application/json',
        'Retry-After' => retry_after(ip, now).to_s
      }, ['{"error":"Rate limit exceeded"}']]
    end

    record_request(ip, now)

    status, headers, body = @app.call(env)

    # Add rate limit headers
    headers['X-RateLimit-Limit'] = @limit.to_s
    headers['X-RateLimit-Remaining'] = remaining(ip).to_s

    [status, headers, body]
  end

  private

  def clean_old_requests(ip, now)
    return unless @requests[ip]
    @requests[ip] = @requests[ip].select { |t| now - t < @window }
  end

  def rate_limited?(ip)
    @requests[ip]&.length.to_i >= @limit
  end

  def record_request(ip, now)
    @requests[ip] ||= []
    @requests[ip] << now
  end

  def remaining(ip)
    @limit - @requests[ip]&.length.to_i
  end

  def retry_after(ip, now)
    oldest = @requests[ip]&.first || now
    @window - (now - oldest)
  end
end

# Usage
use RateLimiter, limit: 100, window: 3600


Practice

Exercise 1: Blog API

Difficulty: Intermediate

Build a blog API with:

  • Posts CRUD
  • Categories and tags
  • Search with filters
  • Pagination

Exercise 2: Authentication System

Difficulty: Advanced

Implement authentication:

  • User registration
  • Login with JWT
  • Password reset
  • Role-based access

Summary

What you learned:

  • Sinatra for minimal web apps
  • Rails routing conventions
  • RESTful resource routing
  • Middleware patterns
  • Controller organization

Next Steps:

  • Read: Ruby OOP
  • Practice: Build a REST API
  • Explore: Rails Active Record

Resources