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 |