CrackedRuby logo

CrackedRuby

REST APIs

Overview

Ruby provides multiple approaches for working with REST APIs, from the built-in Net::HTTP library to third-party gems like HTTParty and Faraday. REST (Representational State Transfer) APIs use HTTP methods (GET, POST, PUT, DELETE) to perform operations on resources identified by URLs.

Ruby's standard library includes Net::HTTP for basic HTTP operations, while JSON and URI libraries handle data serialization and URL manipulation. The ecosystem offers gems that simplify REST API interactions with features like automatic JSON parsing, middleware support, and connection pooling.

require 'net/http'
require 'json'

uri = URI('https://api.example.com/users')
response = Net::HTTP.get_response(uri)
data = JSON.parse(response.body) if response.code == '200'

Ruby applications commonly consume REST APIs for external service integration, data synchronization, and microservice communication. The language's expressiveness and gem ecosystem make it suitable for both API consumption and creation.

# Using HTTParty gem
require 'httparty'

class ApiClient
  include HTTParty
  base_uri 'https://api.example.com'
  
  def get_user(id)
    self.class.get("/users/#{id}")
  end
end

Basic Usage

Net::HTTP forms Ruby's foundation for HTTP communication. The library requires explicit connection management and manual request construction, providing low-level control over HTTP operations.

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

# GET request
uri = URI('https://jsonplaceholder.typicode.com/posts/1')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Get.new(uri)
request['Accept'] = 'application/json'

response = http.request(request)
puts JSON.parse(response.body)['title']

POST requests require body content and appropriate headers. Net::HTTP handles various content types through manual header configuration.

# POST request with JSON body
uri = URI('https://jsonplaceholder.typicode.com/posts')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = JSON.generate({
  title: 'New Post',
  body: 'Post content',
  userId: 1
})

response = http.request(request)
created_post = JSON.parse(response.body)

HTTParty simplifies REST API consumption by providing a DSL for common operations. The gem automatically handles JSON parsing and provides convenient methods for HTTP verbs.

require 'httparty'

class BlogApi
  include HTTParty
  base_uri 'https://jsonplaceholder.typicode.com'
  
  def self.get_posts
    get('/posts')
  end
  
  def self.create_post(title, body, user_id)
    post('/posts', {
      body: {
        title: title,
        body: body,
        userId: user_id
      }.to_json,
      headers: { 'Content-Type' => 'application/json' }
    })
  end
end

posts = BlogApi.get_posts
new_post = BlogApi.create_post('Title', 'Content', 1)

Faraday offers middleware-based HTTP communication with pluggable adapters. The gem supports request/response modification through middleware stacks.

require 'faraday'
require 'faraday/net_http'

conn = Faraday.new(
  url: 'https://api.example.com',
  headers: { 'Content-Type' => 'application/json' }
) do |f|
  f.request :json
  f.response :json
  f.adapter :net_http
end

response = conn.get('/users/1')
user_data = response.body

new_user = conn.post('/users') do |req|
  req.body = { name: 'John', email: 'john@example.com' }
end

Error Handling & Debugging

REST API communication involves multiple failure modes: network timeouts, HTTP errors, malformed JSON, and service unavailability. Ruby provides exception handling mechanisms for each category.

Net::HTTP raises different exception types based on failure conditions. Network-level failures raise socket exceptions, while HTTP-level issues require response code inspection.

require 'net/http'
require 'timeout'
require 'json'

def safe_api_request(uri_string)
  uri = URI(uri_string)
  
  begin
    response = Net::HTTP.start(uri.host, uri.port, 
                               use_ssl: uri.scheme == 'https',
                               open_timeout: 5,
                               read_timeout: 10) do |http|
      request = Net::HTTP::Get.new(uri)
      http.request(request)
    end
    
    case response.code
    when '200'
      JSON.parse(response.body)
    when '404'
      raise "Resource not found: #{uri}"
    when '429'
      raise "Rate limited. Retry after #{response['Retry-After']} seconds"
    when /^5/
      raise "Server error: #{response.code} #{response.message}"
    else
      raise "Unexpected response: #{response.code}"
    end
    
  rescue Timeout::Error
    raise "Request timeout for #{uri}"
  rescue Errno::ECONNREFUSED
    raise "Connection refused to #{uri.host}:#{uri.port}"
  rescue JSON::ParserError => e
    raise "Invalid JSON response: #{e.message}"
  rescue SocketError => e
    raise "Network error: #{e.message}"
  end
end

HTTParty provides response objects with built-in success checking and exception handling options. The gem can raise exceptions on HTTP errors or return response objects for manual inspection.

require 'httparty'

class ApiClient
  include HTTParty
  base_uri 'https://api.example.com'
  timeout 10
  
  def self.get_user_safe(id)
    response = get("/users/#{id}")
    
    if response.success?
      response.parsed_response
    else
      handle_error(response)
    end
  rescue Net::TimeoutError
    { error: 'Request timeout' }
  rescue HTTParty::Error => e
    { error: "HTTP error: #{e.message}" }
  end
  
  private
  
  def self.handle_error(response)
    case response.code
    when 401
      { error: 'Authentication required' }
    when 403
      { error: 'Access forbidden' }
    when 404
      { error: 'User not found' }
    when 422
      { error: 'Validation failed', details: response.parsed_response }
    when 500..599
      { error: 'Server error', code: response.code }
    else
      { error: 'Unknown error', code: response.code }
    end
  end
end

Implementing retry logic handles transient failures common in distributed systems. Exponential backoff prevents overwhelming failing services.

class RetryableApiClient
  MAX_RETRIES = 3
  BASE_DELAY = 1
  
  def self.request_with_retry(uri, max_retries = MAX_RETRIES)
    retries = 0
    
    begin
      yield
    rescue Net::TimeoutError, Errno::ECONNREFUSED, Net::HTTPServerError => e
      retries += 1
      
      if retries <= max_retries
        delay = BASE_DELAY * (2 ** (retries - 1))
        sleep(delay + rand(0.1)) # Add jitter
        retry
      else
        raise "Failed after #{max_retries} retries: #{e.message}"
      end
    end
  end
end

Production Patterns

Production REST API usage requires connection pooling, monitoring, authentication handling, and configuration management. Ruby applications typically centralize API configuration and provide consistent error handling across service boundaries.

Connection pooling reduces overhead for high-throughput applications. Net::HTTP::Persistent maintains persistent connections across requests.

require 'net/http/persistent'
require 'json'

class ProductionApiClient
  def initialize(base_url)
    @base_url = base_url
    @http = Net::HTTP::Persistent.new(name: 'api_client')
    @http.idle_timeout = 30
    @http.retry_change_requests = true
  end
  
  def get(path, headers = {})
    uri = URI.join(@base_url, path)
    
    request = Net::HTTP::Get.new(uri)
    headers.each { |key, value| request[key] = value }
    request['User-Agent'] = "MyApp/1.0"
    
    response = @http.request(uri, request)
    
    log_request(request.method, uri, response.code)
    
    case response.code
    when '200'
      JSON.parse(response.body)
    else
      raise ApiError.new(response.code, response.body)
    end
  rescue JSON::ParserError => e
    raise ApiError.new('parse_error', e.message)
  end
  
  def shutdown
    @http.shutdown
  end
  
  private
  
  def log_request(method, uri, status)
    Rails.logger.info "API #{method} #{uri} #{status}" if defined?(Rails)
  end
end

class ApiError < StandardError
  attr_reader :code, :details
  
  def initialize(code, details)
    @code = code
    @details = details
    super("API Error #{code}: #{details}")
  end
end

Authentication patterns vary by API provider. Token-based authentication requires header management and token refresh logic.

class AuthenticatedApiClient
  def initialize(api_key, secret = nil)
    @api_key = api_key
    @secret = secret
    @access_token = nil
    @token_expires_at = nil
  end
  
  def authenticated_request(method, path, body = nil)
    ensure_valid_token
    
    headers = {
      'Authorization' => "Bearer #{@access_token}",
      'Content-Type' => 'application/json'
    }
    
    case method.upcase
    when 'GET'
      HTTParty.get(build_url(path), headers: headers)
    when 'POST'
      HTTParty.post(build_url(path), headers: headers, body: body.to_json)
    when 'PUT'
      HTTParty.put(build_url(path), headers: headers, body: body.to_json)
    when 'DELETE'
      HTTParty.delete(build_url(path), headers: headers)
    end
  end
  
  private
  
  def ensure_valid_token
    return if @access_token && @token_expires_at > Time.now
    
    auth_response = HTTParty.post(
      build_url('/oauth/token'),
      body: {
        grant_type: 'client_credentials',
        client_id: @api_key,
        client_secret: @secret
      }
    )
    
    if auth_response.success?
      @access_token = auth_response['access_token']
      @token_expires_at = Time.now + auth_response['expires_in'].to_i
    else
      raise "Authentication failed: #{auth_response.code}"
    end
  end
  
  def build_url(path)
    "https://api.example.com#{path}"
  end
end

Configuration management centralizes API settings and supports environment-specific values. Rails applications typically use environment variables and configuration classes.

class ApiConfiguration
  class << self
    def base_url
      ENV.fetch('API_BASE_URL', 'https://api.example.com')
    end
    
    def api_key
      ENV.fetch('API_KEY') { raise 'API_KEY required' }
    end
    
    def timeout
      ENV.fetch('API_TIMEOUT', '30').to_i
    end
    
    def max_retries
      ENV.fetch('API_MAX_RETRIES', '3').to_i
    end
    
    def rate_limit_per_minute
      ENV.fetch('API_RATE_LIMIT', '60').to_i
    end
  end
end

class RateLimitedApiClient
  def initialize
    @requests_this_minute = 0
    @minute_started_at = Time.now
  end
  
  def request(method, path, body = nil)
    enforce_rate_limit
    
    # Make request using configured values
    HTTParty.send(
      method.downcase,
      "#{ApiConfiguration.base_url}#{path}",
      headers: { 'Authorization' => "Bearer #{ApiConfiguration.api_key}" },
      body: body&.to_json,
      timeout: ApiConfiguration.timeout
    )
  end
  
  private
  
  def enforce_rate_limit
    current_time = Time.now
    
    if current_time - @minute_started_at >= 60
      @requests_this_minute = 0
      @minute_started_at = current_time
    end
    
    if @requests_this_minute >= ApiConfiguration.rate_limit_per_minute
      sleep_time = 60 - (current_time - @minute_started_at)
      sleep(sleep_time) if sleep_time > 0
      @requests_this_minute = 0
      @minute_started_at = Time.now
    end
    
    @requests_this_minute += 1
  end
end

Reference

HTTP Client Libraries

Library Installation Primary Use Case Key Features
Net::HTTP Built-in Low-level HTTP control Manual connection management, SSL support
HTTParty gem install httparty Simple REST API consumption DSL, automatic JSON parsing, class-based
Faraday gem install faraday Middleware-based HTTP Pluggable adapters, middleware stack
Rest-Client gem install rest-client Quick REST operations Simple method-based API, built-in JSON

Net::HTTP Methods

Method Parameters Returns Description
Net::HTTP.get(uri) uri (URI/String) String Simple GET request
Net::HTTP.get_response(uri) uri (URI/String) Net::HTTPResponse GET with response object
Net::HTTP.post(uri, data) uri (URI/String), data (String) String Simple POST request
Net::HTTP.start(host, port, opts) host (String), port (Integer), options (Hash) Connection block Persistent connection

HTTParty Methods

Method Parameters Returns Description
get(path, opts) path (String), options (Hash) HTTParty::Response GET request
post(path, opts) path (String), options (Hash) HTTParty::Response POST request
put(path, opts) path (String), options (Hash) HTTParty::Response PUT request
delete(path, opts) path (String), options (Hash) HTTParty::Response DELETE request
base_uri(uri) uri (String) String Set base URL
headers(hash) hash (Hash) Hash Set default headers

Common HTTP Status Codes

Code Meaning Typical Handling
200 OK Parse response body
201 Created Extract created resource
400 Bad Request Check request parameters
401 Unauthorized Refresh authentication
403 Forbidden Check permissions
404 Not Found Handle missing resource
422 Unprocessable Entity Display validation errors
429 Too Many Requests Implement rate limiting
500 Internal Server Error Retry with backoff

JSON Processing

Method Purpose Example
JSON.parse(string) Parse JSON string JSON.parse('{"key":"value"}')
JSON.generate(object) Create JSON string JSON.generate({key: "value"})
to_json Object to JSON {key: "value"}.to_json

Error Classes

Exception Cause Handling Strategy
Timeout::Error Request timeout Retry with backoff
Errno::ECONNREFUSED Connection refused Check service availability
SocketError DNS/network issue Verify host and connectivity
Net::HTTPError HTTP protocol error Inspect response code
JSON::ParserError Invalid JSON Validate response format
URI::InvalidURIError Malformed URL Validate URL construction

Configuration Options

Option Net::HTTP HTTParty Faraday Purpose
Timeout open_timeout, read_timeout timeout request: { timeout: n } Request timeout
SSL use_ssl = true base_uri 'https://' ssl: { verify: true } HTTPS configuration
Headers request['Header'] = 'value' headers class method headers: {} Default headers
User Agent Manual header setting headers 'User-Agent' => 'App' headers: { 'User-Agent' => 'App' } Client identification