← Back to Index

Chapter 5: Network Programming

HTTP requests, API calls & web frameworks

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 = true for HTTPS URLs
  • Set timeouts (open_timeout, read_timeout) to avoid hanging on slow servers
  • Use Net::HTTP.start with 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-Agent header β€” many APIs reject requests without one
  • Handle rate limiting: check X-RateLimit-Remaining headers
  • 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.