1 Net::HTTP Standard Library
Ruby's standard library includes Net::HTTP for making HTTP requests. While its API is more verbose than third-party gems, it requires no extra dependencies β making it ideal for scripts and lightweight applications.
Simple GET Request
require 'net/http'
require 'uri'
require 'json'
uri = URI('https://jsonplaceholder.typicode.com/posts/1')
response = Net::HTTP.get_response(uri)
puts response.code # "200"
puts response.content_type # "application/json"
data = JSON.parse(response.body)
puts data['title']
GET with Headers & Query Parameters
uri = URI('https://api.example.com/users')
uri.query = URI.encode_www_form(page: 1, per_page: 20)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = 5
http.read_timeout = 10
request = Net::HTTP::Get.new(uri)
request['Accept'] = 'application/json'
request['Authorization'] = 'Bearer your_token_here'
response = http.request(request)
case response
when Net::HTTPSuccess
users = JSON.parse(response.body)
users.each { |u| puts "#{u['id']}: #{u['name']}" }
when Net::HTTPRedirection
puts "Redirect to: #{response['location']}"
else
puts "Error: #{response.code} #{response.message}"
end
POST 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 = { title: 'Hello', body: 'World', userId: 1 }.to_json
response = http.request(request)
created = JSON.parse(response.body)
puts "Created post ##{created['id']}"
POST with Form Data
uri = URI('https://httpbin.org/post')
response = Net::HTTP.post_form(uri, name: 'Alice', email: 'alice@example.com')
puts JSON.parse(response.body)['form']
β‘ Net::HTTP Tips
- Always set
use_ssl = truefor HTTPS URLs - Set timeouts (
open_timeout,read_timeout) to avoid hanging on slow servers - Use
Net::HTTP.startwith a block for connection reuse across multiple requests
2 Faraday HTTP Client
Faraday is Ruby's most popular HTTP client gem. It provides a clean, consistent API with middleware support for logging, retries, authentication, and response parsing.
Installation & Basic Usage
# Gemfile
gem 'faraday'
gem 'faraday-retry' # retry middleware
require 'faraday'
require 'json'
response = Faraday.get('https://jsonplaceholder.typicode.com/posts/1')
puts response.status # 200
puts response.headers['content-type']
data = JSON.parse(response.body)
puts data['title']
Configuring a Connection
conn = Faraday.new(url: 'https://api.example.com') do |f|
f.request :json # encode request body as JSON
f.response :json # parse JSON response body
f.response :raise_error # raise on 4xx/5xx
f.request :retry, max: 3,
interval: 0.5,
backoff_factor: 2
f.adapter Faraday.default_adapter
end
conn.headers['Authorization'] = 'Bearer your_token'
GET & POST Requests
response = conn.get('/users', page: 1, per_page: 20)
users = response.body
users.each { |u| puts u['name'] }
response = conn.post('/users') do |req|
req.body = { name: 'Alice', email: 'alice@example.com' }
end
puts "Created: #{response.body['id']}"
response = conn.put("/users/#{user_id}") do |req|
req.body = { name: 'Alice Updated' }
end
conn.delete("/users/#{user_id}")
Error Handling
begin
response = conn.get('/users/999')
puts response.body
rescue Faraday::ResourceNotFound
puts 'User not found (404)'
rescue Faraday::ClientError => e
puts "Client error: #{e.response[:status]}"
rescue Faraday::ServerError
puts 'Server error (5xx), try again later'
rescue Faraday::ConnectionFailed
puts 'Could not connect to server'
rescue Faraday::TimeoutError
puts 'Request timed out'
end
π Cross-Language HTTP Client Comparison
- Ruby Faraday β Python requests β both provide clean APIs with middleware/hooks for customization.
- PHP cURL/Guzzle: Guzzle is PHP's equivalent, with middleware and promise support.
- Node.js fetch/axios: Axios provides interceptors similar to Faraday middleware.
3 JSON Processing
Ruby's standard library includes the json module. It handles parsing and generation, including pretty-printing and symbol key options.
Parse & Generate
require 'json'
json_string = '{"name":"Alice","age":28,"skills":["Ruby","Go","Python"]}'
data = JSON.parse(json_string)
puts data['name'] # "Alice"
puts data['skills'].first # "Ruby"
data_sym = JSON.parse(json_string, symbolize_names: true)
puts data_sym[:name] # "Alice"
hash = { name: 'Bob', age: 32, active: true, tags: %w[dev ruby] }
puts hash.to_json
# {"name":"Bob","age":32,"active":true,"tags":["dev","ruby"]}
puts JSON.pretty_generate(hash)
# {
# "name": "Bob",
# "age": 32,
# "active": true,
# "tags": [
# "dev",
# "ruby"
# ]
# }
Working with API Responses
require 'net/http'
require 'json'
def fetch_posts(limit: 5)
uri = URI("https://jsonplaceholder.typicode.com/posts?_limit=#{limit}")
response = Net::HTTP.get(uri)
posts = JSON.parse(response, symbolize_names: true)
posts.map do |post|
{
id: post[:id],
title: post[:title],
preview: post[:body][0..80]
}
end
end
fetch_posts(limit: 3).each do |post|
puts "##{post[:id]} #{post[:title]}"
puts " #{post[:preview]}..."
puts
end
Safe Parsing
def safe_parse(json_string)
JSON.parse(json_string, symbolize_names: true)
rescue JSON::ParserError => e
puts "Invalid JSON: #{e.message}"
nil
end
safe_parse('{"valid": true}') # => {valid: true}
safe_parse('not json at all') # => nil, prints error
π‘ symbolize_names: true
Using symbolize_names: true converts JSON keys to Ruby symbols (:name instead of "name"). Symbols are immutable and slightly faster for hash lookups, making them the idiomatic choice for internal data structures.
4 Sinatra Web Framework
Sinatra is a lightweight DSL for building web applications in Ruby. It's perfect for APIs, microservices, and prototypes β similar to Python's Flask or Node.js's Express.
Installation & Hello World
# Gemfile
gem 'sinatra'
gem 'sinatra-contrib' # reloader, json helpers
gem 'puma' # production web server
require 'sinatra'
get '/' do
'Hello, World!'
end
get '/hello/:name' do
"Hello, #{params[:name]}!"
end
ruby app.rb
# => Sinatra listening on http://localhost:4567
Routes & Parameters
require 'sinatra'
require 'sinatra/json'
get '/users' do
page = (params[:page] || 1).to_i
per = (params[:per_page] || 20).to_i
json users: fetch_users(page: page, per: per), page: page
end
get '/users/:id' do
user = find_user(params[:id].to_i)
halt 404, json(error: 'User not found') unless user
json user
end
post '/users' do
data = JSON.parse(request.body.read, symbolize_names: true)
user = create_user(data)
status 201
json user
end
put '/users/:id' do
data = JSON.parse(request.body.read, symbolize_names: true)
user = update_user(params[:id].to_i, data)
halt 404, json(error: 'User not found') unless user
json user
end
delete '/users/:id' do
deleted = delete_user(params[:id].to_i)
halt 404, json(error: 'User not found') unless deleted
status 204
end
ERB Templates
get '/dashboard' do
@title = 'Dashboard'
@users = fetch_all_users
erb :dashboard
end
# views/dashboard.erb
# <h1><%= @title %></h1>
# <ul>
# <% @users.each do |user| %>
# <li><%= user[:name] %> - <%= user[:email] %></li>
# <% end %>
# </ul>
JSON API with Error Handling
require 'sinatra/base'
require 'sinatra/json'
class BookAPI < Sinatra::Base
helpers Sinatra::JSON
BOOKS = []
@@next_id = 1
before do
content_type :json
end
get '/api/books' do
json books: BOOKS
end
post '/api/books' do
data = JSON.parse(request.body.read, symbolize_names: true)
halt 422, json(error: 'title is required') unless data[:title]
book = { id: @@next_id, title: data[:title], author: data[:author] }
@@next_id += 1
BOOKS << book
status 201
json book
end
not_found do
json error: 'Resource not found'
end
error do
json error: 'Internal server error'
end
end
π Micro-framework Comparison
- Ruby Sinatra β DSL-style, incredibly concise, great for prototypes and APIs.
- Python Flask β Decorator-based routing, similar philosophy to Sinatra. Flask is heavily inspired by Sinatra.
- Node.js Express β Middleware-centric, more verbose but extremely flexible.
- PHP Slim β PSR-7 based micro-framework, Sinatra-inspired routing.
5 REST API Client Practice
Let's build a real-world API client that consumes the GitHub API, complete with error handling, pagination, and result formatting.
GitHub API Client
require 'faraday'
require 'json'
class GitHubClient
BASE_URL = 'https://api.github.com'
def initialize(token: nil)
@conn = Faraday.new(url: BASE_URL) do |f|
f.request :json
f.response :json
f.response :raise_error
f.headers['Accept'] = 'application/vnd.github.v3+json'
f.headers['Authorization'] = "Bearer #{token}" if token
f.headers['User-Agent'] = 'Ruby-GitHub-Client'
end
end
def user(username)
response = @conn.get("/users/#{username}")
response.body
end
def repos(username, sort: 'updated', per_page: 10)
response = @conn.get("/users/#{username}/repos",
sort: sort, per_page: per_page)
response.body
end
def search_repos(query, sort: 'stars', per_page: 5)
response = @conn.get('/search/repositories',
q: query, sort: sort, per_page: per_page)
response.body
end
end
Using the Client
client = GitHubClient.new
begin
user = client.user('matz')
puts "#{user['name']} (@#{user['login']})"
puts " Repos: #{user['public_repos']}, Followers: #{user['followers']}"
puts "\nTop repos:"
repos = client.repos('matz', sort: 'stargazers_count', per_page: 5)
repos.each do |repo|
stars = repo['stargazers_count']
puts " β #{stars.to_s.rjust(6)} #{repo['name']} β #{repo['description']&.slice(0, 60)}"
end
puts "\nSearching 'ruby web framework':"
results = client.search_repos('ruby web framework')
results['items'].each do |repo|
puts " #{repo['full_name']} (β #{repo['stargazers_count']})"
end
rescue Faraday::ResourceNotFound
puts 'User not found'
rescue Faraday::ClientError => e
puts "API error: #{e.response[:status]} β #{e.response[:body]}"
rescue Faraday::Error => e
puts "Network error: #{e.message}"
end
Pagination Helper
def fetch_all_repos(client, username)
page = 1
all_repos = []
loop do
repos = client.repos(username, per_page: 100)
break if repos.empty?
all_repos.concat(repos)
break if repos.size < 100
page += 1
end
all_repos
end
β‘ API Client Best Practices
- Always set a
User-Agentheader β many APIs reject requests without one - Handle rate limiting: check
X-RateLimit-Remainingheaders - Use retry middleware for transient network failures
- Store API tokens in environment variables, never hardcode them
6 Chapter Summary
π Net::HTTP
Built-in HTTP client β verbose but dependency-free. Good for scripts and simple requests.
π Faraday
Feature-rich HTTP client with middleware for retries, JSON encoding, error handling, and logging.
π JSON
JSON.parse with symbolize_names for reading, .to_json and JSON.pretty_generate for writing.
πΈ Sinatra
Lightweight web framework for APIs and prototypes β DSL routing, ERB templates, JSON helpers.
π§ API Clients
Encapsulate API logic in classes with proper error handling, timeouts, and retry strategies.
π‘οΈ Best Practices
Set timeouts, handle errors gracefully, use environment variables for tokens, respect rate limits.
Next Chapter Preview: Chapter 6 covers Ruby's file operations β reading and writing files, directory traversal, and processing structured data formats like CSV, JSON, and YAML.