CrackedRuby CrackedRuby

Overview

Representational State Transfer (REST) defines an architectural style for distributed hypermedia systems. Roy Fielding introduced REST in his 2000 doctoral dissertation as a set of constraints that, when applied to web architecture, produces desirable properties including scalability, simplicity, modifiability, visibility, portability, and reliability.

REST does not prescribe implementation details or protocols. Rather, REST describes constraints that shape how distributed systems interact. Systems that conform to REST constraints are called RESTful. The web itself represents the largest RESTful system, with browsers acting as clients and web servers providing resources.

RESTful APIs expose resources through URIs (Uniform Resource Identifiers) and allow clients to perform operations on those resources using standard HTTP methods. A resource represents any information that can be named: a document, image, temporal service, collection of other resources, or non-virtual object. Resources remain conceptually separate from the representations returned to clients.

# Resource representation in JSON
{
  "id": 42,
  "title": "RESTful API Design",
  "author": "Jane Smith",
  "published": "2024-03-15"
}

REST gained adoption because it aligns with web infrastructure. HTTP already provides methods, status codes, and headers that map directly to REST operations. Developers familiar with web technologies can build and consume RESTful APIs without learning new protocols or tooling.

The stateless nature of REST simplifies server implementation and improves scalability. Each request contains all information necessary for the server to process it. Servers do not maintain session state between requests, allowing any server in a cluster to handle any request. This constraint enables horizontal scaling by adding more servers without coordination overhead.

Key Principles

REST consists of six architectural constraints. Systems must satisfy all constraints except one optional constraint to be considered RESTful.

Client-Server Architecture

REST requires separation between the client interface and server implementation. Clients initiate requests and servers provide responses. This separation allows client and server to evolve independently. Servers can change internal implementation without affecting clients as long as the interface remains consistent. Multiple client types (web browsers, mobile apps, command-line tools) can interact with the same server API.

Statelessness

Each request from client to server must contain all information needed to understand and process the request. Servers cannot store context between requests. Session state resides entirely on the client. This constraint improves visibility because monitoring systems can understand requests in isolation. It improves reliability because servers can recover from partial failures without concern for lost session data. It improves scalability because servers need not allocate resources to maintain session state.

# Stateless request includes authentication token
GET /api/articles/42
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Accept: application/json

Cacheability

Responses must explicitly indicate whether they can be cached. Clients and intermediaries can cache responses to improve efficiency. Properly implemented caching eliminates some client-server interactions entirely, reducing latency and server load. REST relies on HTTP cache control mechanisms including Cache-Control, ETag, and Last-Modified headers.

Uniform Interface

The uniform interface constraint distinguishes REST from other network application architectures. REST emphasizes a uniform interface between components, simplifying the overall system architecture and improving visibility of interactions. The uniform interface consists of four sub-constraints:

Resource Identification in Requests: Resources are identified by URIs in requests. The resource itself remains conceptually separate from the representation sent to the client. A server might store resources as database rows, files, or computed values, but sends JSON, XML, or HTML representations.

Resource Manipulation Through Representations: Clients manipulate resources by sending representations along with metadata. If a client holds a representation of a resource with metadata, it has sufficient information to modify or delete that resource.

Self-Descriptive Messages: Each message includes enough information to describe how to process the message. Media types describe how to process representations. HTTP methods describe intended operations.

Hypermedia as the Engine of Application State (HATEOAS): Clients interact with applications entirely through hypermedia provided dynamically by servers. Responses include hyperlinks to available actions. Clients need not possess out-of-band information about resource structure or available operations.

Layered System

REST allows an architecture composed of hierarchical layers. Each component cannot see beyond the immediate layer with which it interacts. Layers can encapsulate legacy services, improve security through boundary enforcement, and enable load balancing. Intermediary layers can provide caching, security, or load distribution without client awareness.

Code on Demand (Optional)

Servers can extend client functionality by transferring executable code. Examples include JavaScript sent to web browsers or Java applets. This remains the only optional REST constraint. Most RESTful APIs do not implement code on demand.

Ruby Implementation

Ruby provides multiple frameworks and libraries for building RESTful APIs. Rails and Sinatra represent the most common choices, with Rails offering comprehensive features and conventions while Sinatra provides minimalism and flexibility.

Rails API Mode

Rails includes API-only mode optimized for building RESTful backends without view rendering overhead. API mode excludes middleware and functionality related to browser applications while retaining features needed for API development.

# Generate new Rails API application
rails new my_api --api

# app/controllers/api/v1/articles_controller.rb
module Api
  module V1
    class ArticlesController < ApplicationController
      before_action :set_article, only: [:show, :update, :destroy]

      def index
        @articles = Article.all
        render json: @articles
      end

      def show
        render json: @article
      end

      def create
        @article = Article.new(article_params)
        
        if @article.save
          render json: @article, status: :created, location: api_v1_article_url(@article)
        else
          render json: @article.errors, status: :unprocessable_entity
        end
      end

      def update
        if @article.update(article_params)
          render json: @article
        else
          render json: @article.errors, status: :unprocessable_entity
        end
      end

      def destroy
        @article.destroy
        head :no_content
      end

      private

      def set_article
        @article = Article.find(params[:id])
      end

      def article_params
        params.require(:article).permit(:title, :content, :author)
      end
    end
  end
end

Rails routing conventions map directly to RESTful principles. The resources method generates seven standard routes for a resource.

# config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :articles do
        resources :comments, only: [:index, :create]
      end
    end
  end
end

# Generated routes:
# GET    /api/v1/articles          -> index
# POST   /api/v1/articles          -> create
# GET    /api/v1/articles/:id      -> show
# PATCH  /api/v1/articles/:id      -> update
# PUT    /api/v1/articles/:id      -> update
# DELETE /api/v1/articles/:id      -> destroy

Sinatra for Lightweight APIs

Sinatra offers a minimal DSL for building APIs without Rails conventions or overhead. Sinatra works well for microservices or simple APIs.

require 'sinatra'
require 'json'

# In-memory storage for demonstration
ARTICLES = {}
NEXT_ID = 1

get '/articles' do
  content_type :json
  ARTICLES.values.to_json
end

get '/articles/:id' do
  content_type :json
  article = ARTICLES[params[:id]]
  
  if article
    article.to_json
  else
    status 404
    { error: 'Article not found' }.to_json
  end
end

post '/articles' do
  content_type :json
  data = JSON.parse(request.body.read)
  
  id = NEXT_ID.to_s
  NEXT_ID += 1
  
  article = {
    id: id,
    title: data['title'],
    content: data['content'],
    created_at: Time.now.iso8601
  }
  
  ARTICLES[id] = article
  
  status 201
  headers 'Location' => "#{request.base_url}/articles/#{id}"
  article.to_json
end

put '/articles/:id' do
  content_type :json
  article = ARTICLES[params[:id]]
  
  halt 404, { error: 'Article not found' }.to_json unless article
  
  data = JSON.parse(request.body.read)
  article.merge!(
    title: data['title'],
    content: data['content'],
    updated_at: Time.now.iso8601
  )
  
  article.to_json
end

delete '/articles/:id' do
  article = ARTICLES.delete(params[:id])
  
  if article
    status 204
  else
    status 404
    content_type :json
    { error: 'Article not found' }.to_json
  end
end

Content Negotiation

Ruby APIs should handle multiple content types. The Accept header indicates client preferences, while the Content-Type header specifies the format of request bodies.

# Rails serialization with ActiveModel::Serializers
class ArticleSerializer < ActiveModel::Serializer
  attributes :id, :title, :content, :created_at
  belongs_to :author
  has_many :comments
end

class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    
    respond_to do |format|
      format.json { render json: @article }
      format.xml { render xml: @article }
    end
  end
end

Authentication with JWT

Stateless authentication commonly uses JSON Web Tokens (JWT). The token contains all necessary authentication information, eliminating server-side session storage.

require 'jwt'

class AuthenticationService
  SECRET_KEY = ENV['JWT_SECRET_KEY']
  
  def self.encode(payload, expiration = 24.hours.from_now)
    payload[:exp] = expiration.to_i
    JWT.encode(payload, SECRET_KEY)
  end
  
  def self.decode(token)
    decoded = JWT.decode(token, SECRET_KEY)[0]
    HashWithIndifferentAccess.new(decoded)
  rescue JWT::DecodeError
    nil
  end
end

class ApplicationController < ActionController::API
  before_action :authenticate_request
  
  private
  
  def authenticate_request
    header = request.headers['Authorization']
    token = header.split(' ').last if header
    
    decoded = AuthenticationService.decode(token)
    @current_user = User.find(decoded[:user_id]) if decoded
    
    render json: { error: 'Unauthorized' }, status: :unauthorized unless @current_user
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'Unauthorized' }, status: :unauthorized
  end
end

Practical Examples

Building a Blog API

A blog API demonstrates core REST principles with resources, relationships, and standard operations.

# Models
class Article < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy
  
  validates :title, presence: true, length: { minimum: 5 }
  validates :content, presence: true
end

class Comment < ApplicationRecord
  belongs_to :article
  belongs_to :author, class_name: 'User'
  
  validates :content, presence: true
end

# Routes
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :articles do
        resources :comments, only: [:index, :create, :destroy]
      end
      
      resources :users, only: [:show, :create]
      post 'auth/login', to: 'authentication#login'
    end
  end
end

# Controllers
class Api::V1::ArticlesController < ApplicationController
  skip_before_action :authenticate_request, only: [:index, :show]
  
  def index
    @articles = Article.includes(:author)
                      .order(created_at: :desc)
                      .page(params[:page])
                      .per(params[:per_page] || 20)
    
    render json: @articles, each_serializer: ArticleSerializer,
           meta: pagination_meta(@articles)
  end
  
  def show
    @article = Article.includes(:author, :comments).find(params[:id])
    render json: @article, serializer: ArticleDetailSerializer
  end
  
  def create
    @article = @current_user.articles.build(article_params)
    
    if @article.save
      render json: @article, status: :created,
             location: api_v1_article_url(@article)
    else
      render json: { errors: @article.errors }, status: :unprocessable_entity
    end
  end
  
  private
  
  def article_params
    params.require(:article).permit(:title, :content, :published)
  end
  
  def pagination_meta(collection)
    {
      current_page: collection.current_page,
      total_pages: collection.total_pages,
      total_count: collection.total_count
    }
  end
end

Nested Resource Operations

Comments belong to articles, demonstrating nested resource routing and scoped operations.

class Api::V1::CommentsController < ApplicationController
  before_action :set_article
  
  def index
    @comments = @article.comments.includes(:author).order(created_at: :asc)
    render json: @comments, each_serializer: CommentSerializer
  end
  
  def create
    @comment = @article.comments.build(comment_params)
    @comment.author = @current_user
    
    if @comment.save
      render json: @comment, status: :created,
             location: api_v1_article_comment_url(@article, @comment)
    else
      render json: { errors: @comment.errors }, status: :unprocessable_entity
    end
  end
  
  def destroy
    @comment = @article.comments.find(params[:id])
    
    if @comment.author == @current_user
      @comment.destroy
      head :no_content
    else
      render json: { error: 'Forbidden' }, status: :forbidden
    end
  end
  
  private
  
  def set_article
    @article = Article.find(params[:article_id])
  end
  
  def comment_params
    params.require(:comment).permit(:content)
  end
end

Conditional Requests with ETags

ETags enable efficient caching and conditional updates. The server generates a hash of the resource representation and returns it in the ETag header. Clients include this value in subsequent requests using the If-None-Match header.

class Api::V1::ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    
    if stale?(etag: @article, last_modified: @article.updated_at)
      render json: @article
    end
  end
  
  def update
    @article = Article.find(params[:id])
    
    # Check if resource was modified since client last retrieved it
    if request.headers['If-Match'].present?
      expected_etag = request.headers['If-Match']
      current_etag = Digest::MD5.hexdigest(@article.cache_key_with_version)
      
      if expected_etag != current_etag
        return render json: { error: 'Resource modified by another request' },
                     status: :precondition_failed
      end
    end
    
    if @article.update(article_params)
      render json: @article
    else
      render json: { errors: @article.errors }, status: :unprocessable_entity
    end
  end
end

Design Considerations

Resource Modeling

Resource design represents the most critical decision in REST API development. Resources should model domain concepts rather than database tables. A resource can aggregate multiple entities or represent a computed value.

Resources use nouns, not verbs. URIs identify resources while HTTP methods specify operations. Poor design exposes implementation details or creates verb-based endpoints that violate REST principles.

# Good: noun-based resource URIs
GET    /articles
POST   /articles
GET    /articles/42
PUT    /articles/42
DELETE /articles/42

# Bad: verb-based URIs
GET    /getArticles
POST   /createArticle
GET    /showArticle/42
POST   /updateArticle/42
POST   /deleteArticle/42

Collections and singular resources require different routing strategies. Collections support filtering, sorting, and pagination. Singular resources represent unique entities or singletons.

# Collection resource with query parameters
GET /articles?author=jane&published=true&sort=created_at:desc&page=2

# Singular resource for authenticated user
GET /profile

# Nested resources show relationships
GET /articles/42/comments
POST /articles/42/comments

API Versioning Strategies

REST APIs require versioning to accommodate breaking changes while maintaining backward compatibility. Multiple versioning approaches exist, each with trade-offs.

URI Versioning embeds the version number in the URI path. This approach remains explicit and easy to implement but violates REST principles because the same resource has multiple URIs.

# URI versioning
GET /api/v1/articles/42
GET /api/v2/articles/42

# Rails routing
namespace :api do
  namespace :v1 do
    resources :articles
  end
  
  namespace :v2 do
    resources :articles
  end
end

Header Versioning uses custom headers or the Accept header to specify the API version. This approach keeps URIs clean but remains less visible and requires additional client configuration.

# Custom header versioning
GET /api/articles/42
X-API-Version: 2

# Rails implementation
class ApplicationController < ActionController::API
  before_action :set_api_version
  
  private
  
  def set_api_version
    @api_version = request.headers['X-API-Version'] || '1'
  end
end

# Accept header with media type versioning
GET /api/articles/42
Accept: application/vnd.myapp.v2+json

Content Negotiation Versioning embeds version information in media types. This approach aligns with REST principles but increases complexity.

Version changes should occur infrequently. Additive changes (new fields, new endpoints) do not require version increments. Breaking changes (removing fields, changing field types, altering behavior) necessitate new versions.

HATEOAS Implementation

Hypermedia as the Engine of Application State remains the most frequently ignored REST constraint. HATEOAS requires responses to include links describing available actions. Clients navigate the API by following links rather than constructing URIs.

# HATEOAS response example
{
  "id": 42,
  "title": "RESTful API Design",
  "content": "...",
  "author": {
    "id": 7,
    "name": "Jane Smith",
    "href": "/api/v1/users/7"
  },
  "links": {
    "self": "/api/v1/articles/42",
    "comments": "/api/v1/articles/42/comments",
    "edit": "/api/v1/articles/42",
    "delete": "/api/v1/articles/42"
  }
}

Rails provides helper methods to generate resource URLs programmatically, supporting HATEOAS implementation.

class ArticleSerializer < ActiveModel::Serializer
  attributes :id, :title, :content, :created_at
  
  attribute :links do
    {
      self: api_v1_article_url(object),
      comments: api_v1_article_comments_url(object),
      author: api_v1_user_url(object.author)
    }
  end
  
  def api_v1_article_url(article)
    Rails.application.routes.url_helpers.api_v1_article_url(article)
  end
end

Full HATEOAS implementation remains rare because it increases response size and client complexity. Many APIs adopt a pragmatic middle ground, including links for related resources and available actions without requiring clients to be entirely link-driven.

Idempotency Guarantees

HTTP methods have defined idempotency semantics. Idempotent operations produce the same result regardless of how many times they execute. GET, PUT, DELETE, HEAD, OPTIONS, and TRACE must be idempotent. POST need not be idempotent.

Idempotency protects against network failures and duplicate requests. If a client sends a PUT request but does not receive a response, it can safely retry the request. Multiple identical PUT requests leave the system in the same state as a single request.

POST requests create resources, so multiple identical POST requests create multiple resources. To achieve idempotency for resource creation, clients can use PUT with client-generated identifiers or include idempotency keys.

# Idempotent creation with client-provided ID
PUT /articles/550e8400-e29b-41d4-a716-446655440000
{
  "title": "RESTful API Design",
  "content": "..."
}

# Idempotency with tokens
POST /articles
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

class Api::V1::ArticlesController < ApplicationController
  def create
    idempotency_key = request.headers['Idempotency-Key']
    
    if idempotency_key.present?
      # Check if we've already processed this request
      cached_response = Rails.cache.read("idempotency:#{idempotency_key}")
      return render json: cached_response, status: :ok if cached_response
    end
    
    @article = @current_user.articles.build(article_params)
    
    if @article.save
      response_data = ArticleSerializer.new(@article).as_json
      
      # Cache the response for idempotent replay
      if idempotency_key.present?
        Rails.cache.write("idempotency:#{idempotency_key}", response_data, expires_in: 24.hours)
      end
      
      render json: response_data, status: :created
    else
      render json: { errors: @article.errors }, status: :unprocessable_entity
    end
  end
end

Common Patterns

Pagination

Large collections require pagination to maintain acceptable response times and memory usage. Multiple pagination strategies exist.

Offset-based pagination uses offset and limit parameters. This approach remains simple and familiar but performs poorly on large datasets and suffers from consistency issues when data changes between requests.

# Offset pagination
GET /articles?offset=20&limit=10

class Api::V1::ArticlesController < ApplicationController
  def index
    offset = (params[:offset] || 0).to_i
    limit = [(params[:limit] || 20).to_i, 100].min
    
    @articles = Article.offset(offset).limit(limit)
    
    render json: @articles, meta: {
      offset: offset,
      limit: limit,
      total: Article.count
    }
  end
end

Page-based pagination uses page and per_page parameters. This approach maps naturally to user interfaces but shares offset pagination's performance limitations.

# Page-based pagination with Kaminari gem
GET /articles?page=3&per_page=20

class Api::V1::ArticlesController < ApplicationController
  def index
    page = (params[:page] || 1).to_i
    per_page = [(params[:per_page] || 20).to_i, 100].min
    
    @articles = Article.page(page).per(per_page)
    
    render json: @articles, meta: {
      current_page: @articles.current_page,
      total_pages: @articles.total_pages,
      total_count: @articles.total_count,
      per_page: per_page
    }
  end
end

Cursor-based pagination uses an opaque cursor value to mark position in the result set. This approach performs consistently regardless of dataset size and handles concurrent modifications correctly.

# Cursor pagination
GET /articles?cursor=eyJpZCI6NDJ9&limit=20

class Api::V1::ArticlesController < ApplicationController
  def index
    limit = [(params[:limit] || 20).to_i, 100].min
    
    if params[:cursor].present?
      cursor_data = JSON.parse(Base64.decode64(params[:cursor]))
      @articles = Article.where('id > ?', cursor_data['id'])
                        .order(id: :asc)
                        .limit(limit)
    else
      @articles = Article.order(id: :asc).limit(limit)
    end
    
    next_cursor = if @articles.size == limit
      Base64.encode64({ id: @articles.last.id }.to_json).strip
    else
      nil
    end
    
    render json: @articles, meta: {
      cursor: next_cursor,
      has_more: next_cursor.present?
    }
  end
end

Filtering and Searching

REST APIs commonly support filtering collections based on resource attributes. Query parameters specify filter criteria.

# Basic filtering
GET /articles?author=jane&status=published&year=2024

class Api::V1::ArticlesController < ApplicationController
  def index
    @articles = Article.all
    
    @articles = @articles.where(author: params[:author]) if params[:author].present?
    @articles = @articles.where(status: params[:status]) if params[:status].present?
    @articles = @articles.where('extract(year from created_at) = ?', params[:year]) if params[:year].present?
    
    render json: @articles
  end
end

# Advanced filtering with scopes
class Article < ApplicationRecord
  scope :by_author, ->(author) { where(author: author) }
  scope :published, -> { where(status: 'published') }
  scope :created_in_year, ->(year) { where('extract(year from created_at) = ?', year) }
  scope :search, ->(query) { where('title ILIKE ? OR content ILIKE ?', "%#{query}%", "%#{query}%") }
end

class Api::V1::ArticlesController < ApplicationController
  def index
    @articles = Article.all
    @articles = @articles.by_author(params[:author]) if params[:author]
    @articles = @articles.published if params[:published] == 'true'
    @articles = @articles.created_in_year(params[:year]) if params[:year]
    @articles = @articles.search(params[:q]) if params[:q]
    
    render json: @articles
  end
end

Sorting

Clients specify sort order through query parameters. Multiple sort fields allow complex ordering.

# Sorting examples
GET /articles?sort=created_at
GET /articles?sort=-created_at  # descending
GET /articles?sort=author,created_at  # multiple fields

class Api::V1::ArticlesController < ApplicationController
  SORTABLE_FIELDS = %w[id title author created_at updated_at].freeze
  
  def index
    @articles = Article.all
    
    if params[:sort].present?
      sort_params = params[:sort].split(',')
      
      sort_params.each do |sort_param|
        descending = sort_param.start_with?('-')
        field = descending ? sort_param[1..-1] : sort_param
        
        next unless SORTABLE_FIELDS.include?(field)
        
        direction = descending ? :desc : :asc
        @articles = @articles.order(field => direction)
      end
    else
      @articles = @articles.order(created_at: :desc)
    end
    
    render json: @articles
  end
end

Batch Operations

Some applications require operations on multiple resources simultaneously. REST does not define standard batch semantics, but common patterns exist.

# Batch delete
DELETE /articles
{
  "ids": [1, 2, 3, 4, 5]
}

class Api::V1::ArticlesController < ApplicationController
  def batch_destroy
    ids = params[:ids]
    
    return render json: { error: 'ids parameter required' }, status: :bad_request if ids.blank?
    
    articles = @current_user.articles.where(id: ids)
    destroyed_count = articles.destroy_all.count
    
    render json: {
      deleted_count: destroyed_count,
      requested_count: ids.count
    }
  end
end

# Batch update
PATCH /articles
{
  "updates": [
    { "id": 1, "status": "published" },
    { "id": 2, "status": "published" },
    { "id": 3, "status": "archived" }
  ]
}

def batch_update
  updates = params[:updates]
  results = { success: [], failed: [] }
  
  updates.each do |update|
    article = @current_user.articles.find_by(id: update[:id])
    
    if article && article.update(update.except(:id))
      results[:success] << article.id
    else
      results[:failed] << { id: update[:id], errors: article&.errors || 'not found' }
    end
  end
  
  render json: results
end

Security Implications

Authentication Mechanisms

REST APIs require authentication to protect resources and identify clients. Multiple authentication schemes exist, each appropriate for different scenarios.

Bearer Token Authentication uses tokens passed in the Authorization header. JSON Web Tokens (JWT) represent a common bearer token format. Tokens contain encoded claims and signatures, allowing stateless authentication.

# JWT authentication middleware
class ApplicationController < ActionController::API
  before_action :authenticate_request
  
  attr_reader :current_user
  
  private
  
  def authenticate_request
    header = request.headers['Authorization']
    
    return render json: { error: 'Missing token' }, status: :unauthorized unless header
    
    token = header.split(' ').last
    decoded = AuthenticationService.decode(token)
    
    return render json: { error: 'Invalid token' }, status: :unauthorized unless decoded
    
    @current_user = User.find_by(id: decoded[:user_id])
    
    render json: { error: 'User not found' }, status: :unauthorized unless @current_user
  rescue JWT::ExpiredSignature
    render json: { error: 'Token expired' }, status: :unauthorized
  rescue JWT::DecodeError
    render json: { error: 'Invalid token' }, status: :unauthorized
  end
end

API Key Authentication uses long-lived keys issued to clients. API keys work well for server-to-server communication but should not be exposed in client-side applications.

class ApiKeyAuthenticator
  def self.authenticate(request)
    api_key = request.headers['X-API-Key']
    return nil unless api_key
    
    ApiKey.includes(:user).find_by(key: api_key)&.user
  end
end

class ApplicationController < ActionController::API
  before_action :authenticate_with_api_key
  
  private
  
  def authenticate_with_api_key
    @current_user = ApiKeyAuthenticator.authenticate(request)
    render json: { error: 'Invalid API key' }, status: :unauthorized unless @current_user
  end
end

Authorization Patterns

Authentication identifies users while authorization determines what they can access. Role-based access control (RBAC) and policy-based authorization represent common patterns.

# Role-based authorization
class Article < ApplicationRecord
  belongs_to :author, class_name: 'User'
  
  def editable_by?(user)
    author == user || user.admin?
  end
  
  def deletable_by?(user)
    author == user || user.admin?
  end
end

class Api::V1::ArticlesController < ApplicationController
  def update
    @article = Article.find(params[:id])
    
    unless @article.editable_by?(@current_user)
      return render json: { error: 'Forbidden' }, status: :forbidden
    end
    
    if @article.update(article_params)
      render json: @article
    else
      render json: { errors: @article.errors }, status: :unprocessable_entity
    end
  end
end

# Policy-based authorization with Pundit gem
class ArticlePolicy
  attr_reader :user, :article
  
  def initialize(user, article)
    @user = user
    @article = article
  end
  
  def update?
    user.admin? || article.author == user
  end
  
  def destroy?
    user.admin? || article.author == user
  end
end

class Api::V1::ArticlesController < ApplicationController
  def update
    @article = Article.find(params[:id])
    authorize @article
    
    if @article.update(article_params)
      render json: @article
    else
      render json: { errors: @article.errors }, status: :unprocessable_entity
    end
  end
end

Rate Limiting

Rate limiting protects APIs from abuse and ensures fair resource allocation. Implementation typically uses token bucket or sliding window algorithms.

# Rate limiting with Redis
class RateLimiter
  def initialize(redis = Redis.new)
    @redis = redis
  end
  
  def check_limit(identifier, max_requests, window)
    key = "rate_limit:#{identifier}"
    current = @redis.get(key).to_i
    
    if current >= max_requests
      return { allowed: false, retry_after: @redis.ttl(key) }
    end
    
    @redis.multi do
      @redis.incr(key)
      @redis.expire(key, window) if current == 0
    end
    
    { allowed: true, remaining: max_requests - current - 1 }
  end
end

class ApplicationController < ActionController::API
  before_action :check_rate_limit
  
  private
  
  def check_rate_limit
    limiter = RateLimiter.new
    result = limiter.check_limit(
      rate_limit_identifier,
      max_requests_per_window,
      rate_limit_window
    )
    
    unless result[:allowed]
      response.headers['Retry-After'] = result[:retry_after].to_s
      return render json: { error: 'Rate limit exceeded' }, status: :too_many_requests
    end
    
    response.headers['X-RateLimit-Remaining'] = result[:remaining].to_s
  end
  
  def rate_limit_identifier
    @current_user&.id || request.remote_ip
  end
  
  def max_requests_per_window
    @current_user ? 1000 : 100
  end
  
  def rate_limit_window
    3600  # 1 hour
  end
end

Input Validation and Sanitization

RESTful APIs must validate and sanitize all input to prevent injection attacks, data corruption, and application errors.

class Api::V1::ArticlesController < ApplicationController
  def create
    @article = @current_user.articles.build(article_params)
    
    if @article.save
      render json: @article, status: :created
    else
      render json: { errors: @article.errors.full_messages }, status: :unprocessable_entity
    end
  end
  
  private
  
  def article_params
    params.require(:article).permit(:title, :content, :status, tag_ids: [])
  end
end

class Article < ApplicationRecord
  validates :title, presence: true, length: { minimum: 5, maximum: 200 }
  validates :content, presence: true, length: { minimum: 10 }
  validates :status, inclusion: { in: %w[draft published archived] }
  
  # Sanitize HTML content
  before_save :sanitize_content
  
  private
  
  def sanitize_content
    self.content = ActionController::Base.helpers.sanitize(
      content,
      tags: %w[p br strong em ul ol li a],
      attributes: %w[href]
    )
  end
end

CORS Configuration

Cross-Origin Resource Sharing (CORS) allows browsers to make requests to APIs on different domains. Proper CORS configuration balances security and functionality.

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'https://example.com', 'https://app.example.com'
    
    resource '/api/*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options],
      credentials: true,
      max_age: 86400
  end
  
  # More restrictive settings for public endpoints
  allow do
    origins '*'
    
    resource '/api/v1/public/*',
      headers: :any,
      methods: [:get],
      credentials: false,
      max_age: 3600
  end
end

Reference

HTTP Methods

Method Idempotent Safe Cacheable Purpose
GET Yes Yes Yes Retrieve resource representation
POST No No Only if explicit Create new resource or custom operation
PUT Yes No No Replace entire resource or create at specific URI
PATCH No No No Partially update resource
DELETE Yes No No Remove resource
HEAD Yes Yes Yes Retrieve metadata without body
OPTIONS Yes Yes No Discover available operations

HTTP Status Codes

Code Meaning Usage
200 OK Successful GET, PUT, PATCH, DELETE
201 Created Successful POST creating resource
204 No Content Successful request with no response body
304 Not Modified Conditional GET when resource unchanged
400 Bad Request Malformed request syntax or validation failure
401 Unauthorized Authentication required or failed
403 Forbidden Authenticated but not authorized
404 Not Found Resource does not exist
405 Method Not Allowed HTTP method not supported for resource
409 Conflict Request conflicts with current state
422 Unprocessable Entity Validation errors in request data
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Unexpected server error
503 Service Unavailable Server temporarily unable to handle requests

Response Headers

Header Purpose Example
Content-Type Specifies response format application/json; charset=utf-8
Location URI of newly created resource https://api.example.com/articles/42
ETag Resource version identifier "686897696a7c876b7e"
Last-Modified Resource modification timestamp Wed, 15 Mar 2024 12:00:00 GMT
Cache-Control Caching directives max-age=3600, private
Link Related resources and pagination https://api.example.com/articles?page=2; rel="next"
X-RateLimit-Remaining Remaining requests in window 95
X-RateLimit-Reset Rate limit reset timestamp 1710507600

Request Headers

Header Purpose Example
Accept Preferred response format application/json
Content-Type Request body format application/json
Authorization Authentication credentials Bearer eyJhbGciOiJIUzI1...
If-None-Match Conditional request with ETag "686897696a7c876b7e"
If-Modified-Since Conditional request with timestamp Wed, 15 Mar 2024 12:00:00 GMT
If-Match Conditional update with ETag "686897696a7c876b7e"

Resource URI Patterns

Pattern Example Usage
Collection /articles List all articles
Specific resource /articles/42 Single article
Nested collection /articles/42/comments Comments for article
Nested resource /articles/42/comments/7 Specific comment
Filtering /articles?author=jane&status=published Filtered collection
Pagination /articles?page=2&per_page=20 Page of results
Sorting /articles?sort=-created_at Sorted collection
Searching /articles?q=rest+api Search results

Rails REST Conventions

Action HTTP Method Path Controller Method
List GET /articles index
Create form GET /articles/new new
Create POST /articles create
Show GET /articles/:id show
Edit form GET /articles/:id/edit edit
Update PUT/PATCH /articles/:id update
Delete DELETE /articles/:id destroy

Common Query Parameters

Parameter Purpose Example
page Page number for pagination page=3
per_page Items per page per_page=20
offset Starting position offset=40
limit Maximum items limit=20
cursor Opaque pagination cursor cursor=eyJpZCI6NDJ9
sort Sort order sort=-created_at,title
fields Sparse fieldsets fields=id,title,author
include Include related resources include=author,comments
filter Filter criteria filter[status]=published
q Search query q=rest+api