CrackedRuby CrackedRuby

Overview

HTTP methods define the action a client requests from a server. Each method carries specific semantics that dictate how servers should process requests and what guarantees clients can expect. The HTTP/1.1 specification defines nine methods, though GET, POST, PUT, DELETE, and PATCH handle the majority of web application interactions.

Methods fall into two critical categories: safe methods that do not modify server state (GET, HEAD, OPTIONS) and unsafe methods that may alter resources (POST, PUT, DELETE, PATCH). This distinction affects caching behavior, browser handling, and security considerations.

# Basic HTTP method demonstration
require 'net/http'
uri = URI('https://api.example.com/users/123')

# GET - retrieve resource
response = Net::HTTP.get_response(uri)

# DELETE - remove resource  
request = Net::HTTP::Delete.new(uri)
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

The REST architectural style maps HTTP methods to CRUD operations: GET for reading, POST for creating, PUT for full replacement, PATCH for partial updates, and DELETE for removal. This mapping provides a uniform interface for resource manipulation across distributed systems.

Key Principles

Safe Methods: GET, HEAD, and OPTIONS are defined as safe—they do not modify server state. Servers must not change resources in response to safe method requests. This property allows clients to prefetch resources, enables aggressive caching, and permits link prefetching without side effects. Safe methods are always idempotent.

Idempotent Methods: GET, HEAD, PUT, DELETE, and OPTIONS are idempotent—multiple identical requests produce the same server state as a single request. The response may differ between requests (for example, DELETE returns 200 OK the first time and 404 Not Found subsequently), but the resource state remains consistent. POST and PATCH are not guaranteed idempotent, though implementations may choose to make them so.

Method Semantics by Type:

GET retrieves a representation of a resource. The request has no body, and all parameters pass through the URL query string or path. Responses should be cacheable. GET requests must not trigger side effects like database writes or state changes.

POST submits data to create a new resource or trigger processing. The request body contains the data, and the response typically includes a Location header pointing to the created resource. POST is neither safe nor idempotent—each request may create a new resource or produce different effects.

PUT replaces the entire resource at the specified URI with the request body content. If the resource does not exist, PUT may create it. PUT is idempotent—sending the same PUT request multiple times produces the same result as sending it once. The client specifies the complete resource representation.

PATCH applies partial modifications to a resource. Unlike PUT, PATCH sends only the fields that need updating, not the entire resource. PATCH is not guaranteed idempotent, though JSON Patch and JSON Merge Patch formats provide idempotent semantics.

DELETE removes the resource at the specified URI. DELETE is idempotent—deleting an already-deleted resource leaves the server state unchanged, though the response code may differ. Subsequent DELETE requests typically return 404 Not Found.

HEAD retrieves only the headers for a resource, identical to GET but with no response body. Servers must return the same headers as a GET request would produce. HEAD checks resource existence, validates cache freshness, and determines resource metadata without transferring the full representation.

OPTIONS describes the communication options available for a resource. The response includes allowed methods in the Allow header and may include CORS headers. OPTIONS supports CORS preflight requests that browsers send before cross-origin requests.

# Demonstrating method semantics
class UsersController < ApplicationController
  # GET /users/123 - Safe, idempotent
  def show
    user = User.find(params[:id])
    render json: user
  end
  
  # POST /users - Neither safe nor idempotent
  def create
    user = User.create!(user_params)
    render json: user, status: :created, location: user_url(user)
  end
  
  # PUT /users/123 - Unsafe but idempotent
  def update
    user = User.find(params[:id])
    user.update!(user_params)
    render json: user
  end
  
  # DELETE /users/123 - Unsafe but idempotent
  def destroy
    user = User.find(params[:id])
    user.destroy!
    head :no_content
  end
end

Request and Response Structure: HTTP methods combine with URIs and headers to form complete requests. The method determines whether a request body is allowed (GET and DELETE typically have no body; POST, PUT, and PATCH require bodies). Status codes in responses indicate success (2xx), redirection (3xx), client errors (4xx), or server errors (5xx).

Content Negotiation: Clients specify desired response formats through Accept headers. Servers use Content-Type headers to declare the format of request and response bodies. Common formats include application/json, application/xml, and text/html.

Ruby Implementation

Ruby provides multiple libraries for HTTP method implementation, each with different abstraction levels and feature sets. Net::HTTP ships with Ruby's standard library, while gems like HTTP.rb and Faraday offer more ergonomic interfaces.

Net::HTTP: The standard library implementation provides low-level control over requests. Each HTTP method corresponds to a request class: Net::HTTP::Get, Net::HTTP::Post, Net::HTTP::Put, Net::HTTP::Patch, and Net::HTTP::Delete.

require 'net/http'
require 'json'
require 'uri'

# GET request
uri = URI('https://api.example.com/users/123')
response = Net::HTTP.get_response(uri)
user = JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)

# POST request with JSON body
uri = URI('https://api.example.com/users')
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = JSON.generate({ name: 'Alice', email: 'alice@example.com' })

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

# PUT request for full replacement
uri = URI('https://api.example.com/users/123')
request = Net::HTTP::Put.new(uri)
request['Content-Type'] = 'application/json'
request.body = JSON.generate({ id: 123, name: 'Alice Smith', email: 'alice@example.com' })

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

# PATCH request for partial update
uri = URI('https://api.example.com/users/123')
request = Net::HTTP::Patch.new(uri)
request['Content-Type'] = 'application/json'
request.body = JSON.generate({ email: 'alice.smith@example.com' })

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

# DELETE request
uri = URI('https://api.example.com/users/123')
request = Net::HTTP::Delete.new(uri)
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

HTTP.rb Gem: HTTP.rb provides a chainable interface for building requests. The gem handles connection pooling, automatic decompression, and request timeouts.

require 'http'

# GET with query parameters
response = HTTP.get('https://api.example.com/users', params: { page: 1, limit: 20 })
users = response.parse

# POST with automatic JSON serialization
response = HTTP.post('https://api.example.com/users', 
  json: { name: 'Bob', email: 'bob@example.com' }
)
created_user = response.parse

# PUT with headers
response = HTTP.headers('Authorization' => 'Bearer token123')
              .put('https://api.example.com/users/123',
                json: { name: 'Bob Smith', email: 'bob@example.com' }
              )

# PATCH with partial data
response = HTTP.patch('https://api.example.com/users/123',
  json: { email: 'bob.smith@example.com' }
)

# DELETE with authentication
response = HTTP.auth('Bearer token123')
              .delete('https://api.example.com/users/123')

Faraday Gem: Faraday offers middleware-based architecture for request/response processing. Middleware handles concerns like authentication, logging, retry logic, and caching.

require 'faraday'
require 'faraday/retry'

conn = Faraday.new(url: 'https://api.example.com') do |f|
  f.request :json
  f.request :retry, max: 3, interval: 0.5
  f.response :json
  f.adapter Faraday.default_adapter
end

# GET request
response = conn.get('/users/123')
user = response.body

# POST request
response = conn.post('/users') do |req|
  req.body = { name: 'Carol', email: 'carol@example.com' }
end

# PUT request
response = conn.put('/users/123') do |req|
  req.body = { id: 123, name: 'Carol Jones', email: 'carol@example.com' }
end

# PATCH request
response = conn.patch('/users/123') do |req|
  req.body = { email: 'carol.jones@example.com' }
end

# DELETE request
response = conn.delete('/users/123')

Rails Controller Implementation: Rails maps HTTP methods to controller actions through routing. The framework provides helper methods that respect REST conventions.

# config/routes.rb
Rails.application.routes.draw do
  resources :articles do
    member do
      patch :publish
      post :duplicate
    end
    collection do
      get :archived
    end
  end
end

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  before_action :set_article, only: [:show, :update, :destroy, :publish]

  # GET /articles
  def index
    articles = Article.all
    render json: articles
  end

  # GET /articles/1
  def show
    render json: @article
  end

  # POST /articles
  def create
    article = Article.new(article_params)
    if article.save
      render json: article, status: :created, location: article
    else
      render json: { errors: article.errors }, status: :unprocessable_entity
    end
  end

  # PUT /articles/1 or PATCH /articles/1
  def update
    if @article.update(article_params)
      render json: @article
    else
      render json: { errors: @article.errors }, status: :unprocessable_entity
    end
  end

  # DELETE /articles/1
  def destroy
    @article.destroy
    head :no_content
  end

  # PATCH /articles/1/publish
  def publish
    @article.update!(published_at: Time.current)
    render json: @article
  end

  private

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

  def article_params
    params.require(:article).permit(:title, :body, :author_id)
  end
end

Design Considerations

Method Selection Criteria: Choose methods based on the operation's semantics, not convenience. GET for reading, POST for creation or complex operations, PUT for full replacement, PATCH for partial updates, DELETE for removal. Avoid overloading POST for operations better expressed by other methods.

Use GET when the operation is safe and idempotent. GET requests should never modify server state. Parameters pass through the URL, making requests cacheable and bookmarkable. Browsers prefetch GET requests, and intermediaries cache responses.

Use POST when creating resources without client-specified URIs, when operations have side effects, or when request data is too large or sensitive for URL parameters. POST is the correct choice for operations that do not fit other method semantics, such as searches with complex criteria or actions that trigger processing.

Use PUT when the client provides the complete resource representation and specifies the URI. PUT replaces the entire resource—omitted fields are removed or reset to defaults. PUT is idempotent, making it safe for automatic retry on network failures.

Use PATCH when updating specific fields without affecting others. PATCH is appropriate for large resources where sending the complete representation is wasteful. The request body describes the changes, not the entire resource. Different PATCH formats exist: JSON Patch (RFC 6902) provides fine-grained operations, while JSON Merge Patch (RFC 7396) offers simpler field replacement.

Use DELETE when removing resources. DELETE is idempotent—repeated deletion attempts do not change server state after the first successful deletion. Consider whether to return 204 No Content (delete succeeded) or 404 Not Found (resource already deleted) on subsequent requests.

REST vs RPC Trade-offs: REST constrains method usage to resource-oriented operations. Each URI represents a resource, and methods operate on that resource. This approach provides uniform interfaces and predictable behavior.

RPC-style APIs often overload POST for all operations, tunneling different actions through a single method. While this simplifies client implementation, it loses HTTP semantics benefits: caching, idempotency, and browser handling. RPC is appropriate for operations that do not map to resources, such as complex calculations or multi-step transactions.

# REST-style design
POST   /orders           # Create order
GET    /orders/123       # Retrieve order
PUT    /orders/123       # Replace order
PATCH  /orders/123       # Update order fields
DELETE /orders/123       # Cancel order

# RPC-style design (avoid in REST APIs)
POST /createOrder
POST /getOrder
POST /updateOrder  
POST /deleteOrder

Idempotency in Practice: Design PUT and DELETE operations to be truly idempotent. The first PUT or DELETE changes state; subsequent identical requests leave state unchanged. This property enables automatic retry without duplicate effects.

Idempotency requires careful database design. Use unique constraints to prevent duplicate creation. Implement conditional updates that check current state before modifying resources. Generate idempotency keys for POST operations that should not create duplicates.

# Non-idempotent PATCH (problematic)
def update
  article = Article.find(params[:id])
  article.increment!(:view_count)  # Each request increments
  render json: article
end

# Idempotent PUT (correct)
def update
  article = Article.find(params[:id])
  article.update!(article_params)  # Sets absolute value
  render json: article
end

# Idempotency key for POST
def create
  key = request.headers['Idempotency-Key']
  
  # Check if request with this key already processed
  existing = IdempotencyRecord.find_by(key: key)
  return render json: existing.response, status: existing.status if existing
  
  # Process request and store result
  article = Article.create!(article_params)
  IdempotencyRecord.create!(
    key: key,
    response: article.to_json,
    status: 201
  )
  
  render json: article, status: :created
end

Caching Implications: Safe methods (GET, HEAD, OPTIONS) are cacheable by default. Responses should include appropriate Cache-Control, ETag, and Last-Modified headers. Unsafe methods invalidate cached entries for the modified resource.

PUT and PATCH responses should include updated resource representations or return 204 No Content. DELETE should return 204 No Content or 200 OK with a final resource representation. These responses help clients maintain consistent cache state.

Practical Examples

Building a RESTful API Client: A complete HTTP client handles all methods, error responses, authentication, and retry logic.

require 'http'

class APIClient
  def initialize(base_url, api_key)
    @base_url = base_url
    @api_key = api_key
    @http = HTTP.headers(
      'Authorization' => "Bearer #{api_key}",
      'Accept' => 'application/json'
    ).timeout(connect: 5, read: 10)
  end

  def get(path, params: {})
    response = @http.get("#{@base_url}#{path}", params: params)
    handle_response(response)
  end

  def post(path, body:)
    response = @http.post("#{@base_url}#{path}", json: body)
    handle_response(response)
  end

  def put(path, body:)
    response = @http.put("#{@base_url}#{path}", json: body)
    handle_response(response)
  end

  def patch(path, body:)
    response = @http.patch("#{@base_url}#{path}", json: body)
    handle_response(response)
  end

  def delete(path)
    response = @http.delete("#{@base_url}#{path}")
    handle_response(response)
  end

  private

  def handle_response(response)
    case response.code
    when 200..299
      response.parse
    when 404
      raise NotFoundError, "Resource not found"
    when 422
      raise ValidationError, response.parse['errors']
    when 500..599
      raise ServerError, "Server error: #{response.code}"
    else
      raise APIError, "Unexpected response: #{response.code}"
    end
  end
end

# Usage
client = APIClient.new('https://api.example.com', 'your-api-key')

# GET - list resources
users = client.get('/users', params: { page: 1, limit: 20 })

# POST - create resource
new_user = client.post('/users', body: {
  name: 'David',
  email: 'david@example.com',
  role: 'admin'
})

# PUT - replace resource
updated_user = client.put("/users/#{new_user['id']}", body: {
  id: new_user['id'],
  name: 'David Brown',
  email: 'david.brown@example.com',
  role: 'admin'
})

# PATCH - partial update
partially_updated = client.patch("/users/#{new_user['id']}", body: {
  email: 'david.b@example.com'
})

# DELETE - remove resource
client.delete("/users/#{new_user['id']}")

Implementing JSON Patch for PATCH Requests: JSON Patch (RFC 6902) provides precise control over partial updates through an array of operations.

require 'json'
require 'http'

class JSONPatchClient
  def initialize(base_url)
    @base_url = base_url
  end

  def patch(path, operations)
    response = HTTP.headers('Content-Type' => 'application/json-patch+json')
                   .patch("#{@base_url}#{path}", json: operations)
    response.parse if response.status.success?
  end
end

client = JSONPatchClient.new('https://api.example.com')

# Replace a field value
client.patch('/users/123', [
  { op: 'replace', path: '/email', value: 'newemail@example.com' }
])

# Add an element to an array
client.patch('/users/123', [
  { op: 'add', path: '/roles/-', value: 'moderator' }
])

# Remove a field
client.patch('/users/123', [
  { op: 'remove', path: '/temporary_token' }
])

# Multiple operations in one request
client.patch('/users/123', [
  { op: 'replace', path: '/name', value: 'Eva Martinez' },
  { op: 'replace', path: '/email', value: 'eva.martinez@example.com' },
  { op: 'add', path: '/verified', value: true }
])

# Server-side JSON Patch implementation
class UsersController < ApplicationController
  def update
    if request.content_type == 'application/json-patch+json'
      apply_json_patch
    else
      apply_standard_update
    end
  end

  private

  def apply_json_patch
    user = User.find(params[:id])
    operations = JSON.parse(request.body.read)
    
    operations.each do |op|
      case op['op']
      when 'replace'
        field = op['path'].delete_prefix('/')
        user.update!(field => op['value'])
      when 'add'
        field = op['path'].delete_prefix('/').sub('/-', '')
        if user.send(field).is_a?(Array)
          user.send(field) << op['value']
          user.save!
        else
          user.update!(field => op['value'])
        end
      when 'remove'
        field = op['path'].delete_prefix('/')
        user.update!(field => nil)
      end
    end
    
    render json: user
  end

  def apply_standard_update
    user = User.find(params[:id])
    user.update!(user_params)
    render json: user
  end
end

Conditional Requests with ETags: ETags enable efficient caching and prevent lost updates through conditional HTTP requests.

class ArticlesController < ApplicationController
  # GET /articles/1
  def show
    article = Article.find(params[:id])
    
    # Generate ETag from resource state
    etag = Digest::MD5.hexdigest("#{article.id}-#{article.updated_at.to_i}")
    
    # Check If-None-Match header
    if request.headers['If-None-Match'] == etag
      head :not_modified
      return
    end
    
    response.headers['ETag'] = etag
    response.headers['Cache-Control'] = 'private, max-age=300'
    render json: article
  end

  # PUT /articles/1
  def update
    article = Article.find(params[:id])
    current_etag = Digest::MD5.hexdigest("#{article.id}-#{article.updated_at.to_i}")
    
    # Verify If-Match header to prevent lost updates
    if request.headers['If-Match'].present?
      if request.headers['If-Match'] != current_etag
        render json: { error: 'Resource has been modified' }, 
               status: :precondition_failed
        return
      end
    end
    
    article.update!(article_params)
    
    new_etag = Digest::MD5.hexdigest("#{article.id}-#{article.updated_at.to_i}")
    response.headers['ETag'] = new_etag
    
    render json: article
  end
end

# Client usage
require 'http'

# Initial GET with ETag
response = HTTP.get('https://api.example.com/articles/123')
etag = response.headers['ETag']
article = response.parse

# Subsequent GET with If-None-Match
response = HTTP.headers('If-None-Match' => etag)
              .get('https://api.example.com/articles/123')

if response.code == 304
  # Use cached article, server returned not modified
  puts "Using cached version"
else
  # Server returned updated article
  article = response.parse
  etag = response.headers['ETag']
end

# PUT with If-Match to prevent lost updates
article['title'] = 'Updated Title'
response = HTTP.headers('If-Match' => etag)
              .put('https://api.example.com/articles/123', json: article)

if response.code == 412
  # Precondition failed, article was modified by someone else
  puts "Conflict detected, need to fetch latest version"
end

Handling Method Override: Some clients cannot send PUT, PATCH, or DELETE methods. Method override through POST with a header or form field provides a workaround.

# Middleware to handle method override
class MethodOverride
  def initialize(app)
    @app = app
  end

  def call(env)
    if env['REQUEST_METHOD'] == 'POST'
      # Check _method parameter
      request = Rack::Request.new(env)
      method = request.params['_method']
      
      # Or check X-HTTP-Method-Override header
      method ||= env['HTTP_X_HTTP_METHOD_OVERRIDE']
      
      if method && %w[PUT PATCH DELETE].include?(method.upcase)
        env['REQUEST_METHOD'] = method.upcase
      end
    end
    
    @app.call(env)
  end
end

# HTML form with method override
# <form action="/articles/123" method="post">
#   <input type="hidden" name="_method" value="patch">
#   <input type="text" name="title">
#   <button type="submit">Update</button>
# </form>

# JavaScript client with method override header
fetch('https://api.example.com/articles/123', {
  method: 'POST',
  headers: {
    'X-HTTP-Method-Override': 'PATCH',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ title: 'New Title' })
})

Security Implications

CSRF Protection for Unsafe Methods: Cross-Site Request Forgery exploits browser behavior that automatically includes cookies with requests. Unsafe methods (POST, PUT, PATCH, DELETE) require CSRF tokens to verify requests originate from the application.

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  
  # For APIs, use token authentication instead of session-based CSRF
  protect_from_forgery with: :null_session, if: -> { request.format.json? }
  
  before_action :verify_api_token, if: -> { request.format.json? }
  
  private
  
  def verify_api_token
    token = request.headers['Authorization']&.split(' ')&.last
    
    unless valid_token?(token)
      render json: { error: 'Unauthorized' }, status: :unauthorized
    end
  end
  
  def valid_token?(token)
    # Verify JWT or API key
    token.present? && APIKey.exists?(token: token, active: true)
  end
end

# JavaScript client with CSRF token
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

fetch('/articles', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify({ title: 'New Article' })
});

Preventing Method-Based Attacks: Validate that HTTP methods match the intended operation. Reject GET requests that attempt to modify state. Block unsafe methods on read-only resources.

class ArticlesController < ApplicationController
  before_action :ensure_read_only, only: [:show, :index]
  before_action :require_unsafe_method, only: [:create, :update, :destroy]
  
  private
  
  def ensure_read_only
    unless request.get? || request.head?
      render json: { error: 'Method not allowed' }, status: :method_not_allowed
    end
  end
  
  def require_unsafe_method
    if request.get? || request.head?
      render json: { error: 'Unsafe operation requires POST/PUT/PATCH/DELETE' },
             status: :method_not_allowed
    end
  end
end

Rate Limiting by Method: Apply different rate limits to safe and unsafe methods. GET requests tolerate higher rates than POST, PUT, PATCH, or DELETE. Malicious actors often abuse unsafe methods to overload servers or create spam.

class RateLimitMiddleware
  LIMITS = {
    'GET' => 1000,     # requests per hour
    'POST' => 100,
    'PUT' => 100,
    'PATCH' => 100,
    'DELETE' => 50
  }.freeze

  def initialize(app)
    @app = app
  end

  def call(env)
    method = env['REQUEST_METHOD']
    client_id = extract_client_id(env)
    
    key = "rate_limit:#{client_id}:#{method}"
    count = REDIS.incr(key)
    REDIS.expire(key, 3600) if count == 1
    
    limit = LIMITS[method] || 100
    
    if count > limit
      return [429, { 'Content-Type' => 'application/json' }, 
              [{ error: 'Rate limit exceeded' }.to_json]]
    end
    
    @app.call(env)
  end
  
  private
  
  def extract_client_id(env)
    request = Rack::Request.new(env)
    request.ip
  end
end

Authorization for Different Methods: Require stronger authorization for destructive operations. Read operations may permit broader access than write operations.

class ArticlesController < ApplicationController
  before_action :set_article, except: [:index, :create]
  before_action :authorize_read, only: [:index, :show]
  before_action :authorize_write, only: [:create, :update]
  before_action :authorize_delete, only: [:destroy]
  
  private
  
  def authorize_read
    unless current_user&.can_read?(Article)
      render json: { error: 'Forbidden' }, status: :forbidden
    end
  end
  
  def authorize_write
    unless current_user&.can_write?(Article)
      render json: { error: 'Forbidden' }, status: :forbidden
    end
  end
  
  def authorize_delete
    unless current_user&.admin? || @article.author == current_user
      render json: { error: 'Forbidden' }, status: :forbidden
    end
  end
end

Secure DELETE Implementation: DELETE operations should verify ownership or permissions before removing resources. Consider soft deletion for important data.

class UsersController < ApplicationController
  def destroy
    user = User.find(params[:id])
    
    # Verify authorization
    unless current_user.admin? || current_user.id == user.id
      return render json: { error: 'Forbidden' }, status: :forbidden
    end
    
    # Prevent accidental deletion through confirmation
    unless request.headers['X-Confirm-Delete'] == 'true'
      return render json: { 
        error: 'Include X-Confirm-Delete: true header to confirm'
      }, status: :precondition_required
    end
    
    # Soft delete instead of permanent removal
    user.update!(deleted_at: Time.current)
    
    # Or implement permanent deletion with audit trail
    AuditLog.create!(
      action: 'delete',
      resource_type: 'User',
      resource_id: user.id,
      performed_by: current_user.id
    )
    user.destroy!
    
    head :no_content
  end
end

Common Pitfalls

Using GET for Operations with Side Effects: GET requests must not modify server state. Browsers prefetch GET requests, cache responses, and include them in history. Using GET for deletions or state changes creates security vulnerabilities.

# WRONG - GET with side effects
get '/articles/:id/delete' do
  Article.find(params[:id]).destroy
  redirect '/articles'
end

# CORRECT - DELETE method
delete '/articles/:id' do
  Article.find(params[:id]).destroy
  status 204
end

# WRONG - GET for form submission
get '/subscribe' do
  Subscription.create!(email: params[:email])
  "Subscribed!"
end

# CORRECT - POST for creation
post '/subscribe' do
  Subscription.create!(email: params[:email])
  status 201
end

Sending Request Bodies with GET or DELETE: While HTTP specifications do not prohibit request bodies for GET or DELETE, many servers and intermediaries ignore or reject them. Parameters for GET must pass through the URL. DELETE typically has no body.

# PROBLEMATIC - DELETE with body (may not work)
HTTP.delete('https://api.example.com/users/123', json: { reason: 'spam' })

# BETTER - DELETE with query parameters
HTTP.delete('https://api.example.com/users/123?reason=spam')

# WRONG - GET with body
HTTP.get('https://api.example.com/users', json: { filter: 'active' })

# CORRECT - GET with query string
HTTP.get('https://api.example.com/users', params: { filter: 'active' })

Misunderstanding PUT vs PATCH: PUT requires the complete resource representation. Omitted fields are removed or reset. PATCH updates only specified fields. Mixing these semantics causes data loss.

# Current resource: { id: 123, name: 'Frank', email: 'frank@example.com', role: 'admin' }

# WRONG - Using PUT for partial update
HTTP.put('https://api.example.com/users/123', 
  json: { email: 'frank.new@example.com' }
)
# Result: { id: 123, email: 'frank.new@example.com', name: nil, role: nil }
# Data loss! PUT replaced entire resource with incomplete data

# CORRECT - Using PATCH for partial update  
HTTP.patch('https://api.example.com/users/123',
  json: { email: 'frank.new@example.com' }
)
# Result: { id: 123, name: 'Frank', email: 'frank.new@example.com', role: 'admin' }

# CORRECT - Using PUT with complete resource
HTTP.put('https://api.example.com/users/123',
  json: { id: 123, name: 'Frank', email: 'frank.new@example.com', role: 'admin' }
)

Not Handling Idempotency Properly: Treating idempotent methods as non-idempotent breaks retry logic and confuses clients. Multiple identical DELETE requests should produce the same final state, even if response codes differ.

# WRONG - Non-idempotent DELETE
def destroy
  article = Article.find(params[:id])  # Raises if already deleted
  article.destroy
  head :no_content
end
# Second DELETE raises 404, breaks idempotency contract

# CORRECT - Idempotent DELETE
def destroy
  article = Article.find_by(id: params[:id])
  article&.destroy
  head :no_content  # Always returns 204, even if already deleted
end

# WRONG - Non-idempotent PUT with side effects
def update
  user = User.find(params[:id])
  user.increment!(:login_count)  # Increments every time
  user.update!(user_params)
  render json: user
end

# CORRECT - Idempotent PUT
def update
  user = User.find(params[:id])
  user.update!(user_params)  # Sets absolute values
  render json: user
end

Ignoring Status Codes for Different Methods: Each method has appropriate success status codes. POST returns 201 Created with a Location header. PUT and PATCH return 200 OK with the updated resource or 204 No Content. DELETE returns 204 No Content or 200 OK.

# WRONG - Returning 200 for POST
def create
  article = Article.create!(article_params)
  render json: article, status: :ok  # Should be 201
end

# CORRECT - POST returns 201 with Location
def create
  article = Article.create!(article_params)
  response.headers['Location'] = article_url(article)
  render json: article, status: :created
end

# WRONG - Returning 200 with body for DELETE
def destroy
  article = Article.find(params[:id])
  article.destroy
  render json: article  # Wasteful, DELETE typically has no body
end

# CORRECT - DELETE returns 204
def destroy
  article = Article.find(params[:id])
  article.destroy
  head :no_content
end

Overloading POST for Everything: Using POST for all operations loses HTTP semantics. Caching fails, idempotency guarantees disappear, and clients cannot retry safely.

# WRONG - POST for reading
post '/get-user' do
  user = User.find(params[:id])
  render json: user
end

# CORRECT - GET for reading
get '/users/:id' do
  user = User.find(params[:id])
  render json: user
end

# WRONG - POST for updates
post '/update-user' do
  user = User.find(params[:id])
  user.update!(params[:data])
  render json: user
end

# CORRECT - PUT or PATCH for updates
patch '/users/:id' do
  user = User.find(params[:id])
  user.update!(user_params)
  render json: user
end

Not Implementing OPTIONS for CORS: Single-page applications making cross-origin requests require OPTIONS preflight responses. Missing or incorrect OPTIONS handling breaks browser requests.

# WRONG - No OPTIONS support
class ArticlesController < ApplicationController
  def index
    render json: Article.all
  end
end

# CORRECT - OPTIONS support for CORS
class ArticlesController < ApplicationController
  before_action :set_cors_headers
  
  def index
    render json: Article.all
  end
  
  def options
    head :ok
  end
  
  private
  
  def set_cors_headers
    headers['Access-Control-Allow-Origin'] = '*'
    headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
    headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
    headers['Access-Control-Max-Age'] = '3600'
  end
end

# config/routes.rb
resources :articles do
  collection do
    match '', action: :options, via: :options
  end
  member do
    match '', action: :options, via: :options
  end
end

Reference

HTTP Method Quick Reference

Method Safe Idempotent Request Body Response Body Cache Common Status Codes
GET Yes Yes No Yes Yes 200, 304, 404
HEAD Yes Yes No No Yes 200, 304, 404
POST No No Yes Yes No 201, 200, 400, 422
PUT No Yes Yes Yes No 200, 204, 404
PATCH No No Yes Yes No 200, 204, 404
DELETE No Yes No Optional No 204, 200, 404
OPTIONS Yes Yes No Yes No 200, 204

Ruby HTTP Library Comparison

Feature Net::HTTP HTTP.rb Faraday
Standard Library Yes No No
Chainable Interface No Yes Yes
Connection Pooling Manual Yes Via Adapter
Automatic Retry No No Via Middleware
Timeout Support Yes Yes Yes
JSON Serialization Manual Automatic Via Middleware
Middleware No No Yes
Streaming Yes Yes Yes

REST Method to CRUD Mapping

HTTP Method CRUD Operation SQL Equivalent Idempotent
GET Read SELECT Yes
POST Create INSERT No
PUT Update/Replace UPDATE Yes
PATCH Update/Modify UPDATE No
DELETE Delete DELETE Yes

Status Codes by Method

Method Success Codes Client Error Server Error
GET 200 OK, 304 Not Modified 404 Not Found 500 Internal Server Error
POST 201 Created, 200 OK 400 Bad Request, 422 Unprocessable Entity 500 Internal Server Error
PUT 200 OK, 204 No Content 404 Not Found, 400 Bad Request 500 Internal Server Error
PATCH 200 OK, 204 No Content 404 Not Found, 400 Bad Request 500 Internal Server Error
DELETE 204 No Content, 200 OK 404 Not Found 500 Internal Server Error
OPTIONS 200 OK, 204 No Content 404 Not Found 500 Internal Server Error

Request Headers by Method

Header GET POST PUT PATCH DELETE Purpose
Content-Type Rare Required Required Required Rare Request body format
Accept Common Common Common Common Common Desired response format
If-Match No No Common Common Common Conditional request ETag
If-None-Match Common No No No No Cache validation
Authorization Common Common Common Common Common Authentication credentials
Idempotency-Key No Common No No No Prevent duplicate operations

Response Headers by Method

Header GET POST PUT PATCH DELETE Purpose
Content-Type Required Required Optional Optional Rare Response body format
ETag Common Common Common Common No Resource version identifier
Location No Required Optional No No Created/updated resource URI
Cache-Control Common Rare Rare Rare No Caching directives
Last-Modified Common No Optional Optional No Resource modification time

Rails Route Helpers by Method

HTTP Method Route Helper Controller Action URL Pattern
GET resources index /resources
GET resources show /resources/:id
POST resources create /resources
PUT/PATCH resources update /resources/:id
DELETE resources destroy /resources/:id
GET new_resource new /resources/new
GET edit_resource edit /resources/:id/edit