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 |