Skip to content

Ruby Web Services

Web services are an important component of modern application architecture, allowing different systems to communicate and exchange data over the network. Ruby provides multiple ways to create and consume Web services, from simple HTTP servers to complex RESTful APIs. This chapter will explain in detail how to create Web services in Ruby, including using built-in libraries and popular frameworks like Sinatra and Rails.

🎯 Web Service Basics

What is a Web Service

A Web service is a software system that provides functionality over the network, using standardized protocols (such as HTTP, XML, JSON) for communication. The main characteristics of Web services include:

  • Interoperability: Systems on different platforms and languages can call each other
  • Loose Coupling: Minimal dependency between service providers and consumers
  • Reusability: The same service can be used by multiple applications
  • Discoverability: Services can be discovered and called through standard methods

Web Service Types

  1. SOAP Web Services: XML-based protocol using WSDL to describe services
  2. RESTful Web Services: HTTP-based protocol using JSON/XML for data transfer
  3. GraphQL: Modern API query language allowing clients to precisely specify needed data

🌐 Using Built-in Libraries to Create Web Services

Basic HTTP Server

ruby
require 'webrick'

# Create simple HTTP server
class SimpleHTTPServer
  def initialize(port = 8080)
    @port = port
    @server = nil
  end
  
  def start
    # Configure server
    config = {
      Port: @port,
      DocumentRoot: './public'
    }
    
    @server = WEBrick::HTTPServer.new(config)
    
    # Define route handlers
    @server.mount_proc('/hello') do |req, res|
      res.body = 'Hello, World!'
      res['Content-Type'] = 'text/plain'
    end
    
    @server.mount_proc('/api/time') do |req, res|
      res.body = { 
        time: Time.now.to_s,
        timestamp: Time.now.to_i
      }.to_json
      res['Content-Type'] = 'application/json'
    end
    
    # Handle POST requests
    @server.mount_proc('/api/echo') do |req, res|
      res.body = {
        method: req.request_method,
        path: req.path,
        headers: req.header,
        body: req.body
      }.to_json
      res['Content-Type'] = 'application/json'
    end
    
    # Graceful shutdown
    trap('INT') { @server.shutdown }
    
    puts "Server started, listening on port #{@port}"
    @server.start
  end
end

# Start server
# server = SimpleHTTPServer.new(8080)
# server.start

RESTful API Server

ruby
require 'webrick'
require 'json'

# Simple in-memory database
class InMemoryDB
  def initialize
    @data = {}
    @next_id = 1
  end
  
  def create(item)
    id = @next_id
    @next_id += 1
    @data[id] = item.merge('id' => id, 'created_at' => Time.now.to_i)
    @data[id]
  end
  
  def find(id)
    @data[id.to_i]
  end
  
  def all
    @data.values
  end
  
  def update(id, item)
    return nil unless @data[id.to_i]
    @data[id.to_i] = item.merge('id' => id.to_i, 'updated_at' => Time.now.to_i)
    @data[id.to_i]
  end
  
  def delete(id)
    @data.delete(id.to_i)
  end
end

# RESTful API handler
class RESTAPIHandler < WEBrick::HTTPServlet::AbstractServlet
  def initialize(server, db)
    super(server)
    @db = db
  end
  
  def do_GET(request, response)
    case request.path
    when '/api/users'
      # Get all users
      users = @db.all
      send_json_response(response, users, 200)
      
    when %r{/api/users/(\d+)}
      # Get specific user
      id = $1
      user = @db.find(id)
      if user
        send_json_response(response, user, 200)
      else
        send_error_response(response, 'User not found', 404)
      end
      
    else
      send_error_response(response, 'Path not found', 404)
    end
  end
  
  def do_POST(request, response)
    case request.path
    when '/api/users'
      # Create user
      begin
        user_data = JSON.parse(request.body)
        user = @db.create(user_data)
        send_json_response(response, user, 201)
      rescue JSON::ParserError
        send_error_response(response, 'Invalid JSON format', 400)
      end
      
    else
      send_error_response(response, 'Path not found', 404)
    end
  end
  
  def do_PUT(request, response)
    case request.path
    when %r{/api/users/(\d+)}
      # Update user
      id = $1
      begin
        user_data = JSON.parse(request.body)
        user = @db.update(id, user_data)
        if user
          send_json_response(response, user, 200)
        else
          send_error_response(response, 'User not found', 404)
        end
      rescue JSON::ParserError
        send_error_response(response, 'Invalid JSON format', 400)
      end
      
    else
      send_error_response(response, 'Path not found', 404)
    end
  end
  
  def do_DELETE(request, response)
    case request.path
    when %r{/api/users/(\d+)}
      # Delete user
      id = $1
      user = @db.delete(id)
      if user
        send_json_response(response, { 'message' => 'User deleted' }, 200)
      else
        send_error_response(response, 'User not found', 404)
      end
      
    else
      send_error_response(response, 'Path not found', 404)
    end
  end
  
  private
  
  def send_json_response(response, data, status)
    response.body = data.to_json
    response['Content-Type'] = 'application/json'
    response.status = status
  end
  
  def send_error_response(response, message, status)
    response.body = { 'error' => message }.to_json
    response['Content-Type'] = 'application/json'
    response.status = status
  end
end

# Start RESTful API server
# db = InMemoryDB.new
# server = WEBrick::HTTPServer.new(Port: 8080)
# server.mount('/api', RESTAPIHandler, db)

🎵 Using Sinatra to Create Web Services

Sinatra Basics

Sinatra is a lightweight Ruby Web framework, perfect for creating RESTful APIs and small Web applications:

ruby
# First install Sinatra
# gem install sinatra

require 'sinatra'
require 'json'

# Basic Sinatra app
get '/' do
  'Hello, Sinatra!'
end

get '/hello/:name' do
  "Hello, #{params[:name]}!"
end

# JSON API
get '/api/time' do
  content_type :json
  {
    time: Time.now.to_s,
    timestamp: Time.now.to_i
  }.to_json
end

# RESTful routes
get '/api/users' do
  content_type :json
  User.all.to_json
end

get '/api/users/:id' do
  content_type :json
  user = User.find(params[:id])
  if user
    user.to_json
  else
    status 404
    { error: 'User not found' }.to_json
  end
end

post '/api/users' do
  content_type :json
  user = User.create(JSON.parse(request.body.read))
  status 201
  user.to_json
end

put '/api/users/:id' do
  content_type :json
  user = User.update(params[:id], JSON.parse(request.body.read))
  if user
    user.to_json
  else
    status 404
    { error: 'User not found' }.to_json
  end
end

delete '/api/users/:id' do
  content_type :json
  if User.destroy(params[:id])
    { message: 'User deleted' }.to_json
  else
    status 404
    { error: 'User not found' }.to_json
  end
end

Sinatra with Database

ruby
require 'sinatra'
require 'sinatra/activerecord'
require 'json'

set :database, 'sqlite3:development.sqlite3'

# Define model
class User < ActiveRecord::Base
  validates :name, presence: true
  validates :email, presence: true, uniqueness: true
end

# API endpoints
get '/api/users' do
  content_type :json
  User.all.to_json
end

post '/api/users' do
  content_type :json
  user = User.create(JSON.parse(request.body.read))
  if user.persisted?
    status 201
    user.to_json
  else
    status 422
    { errors: user.errors.full_messages }.to_json
  end
end

🚀 Using Rails to Create Web Services

Rails API Mode

bash
# Create new Rails API
rails new my_api --api

# Or add API mode to existing Rails app
# rails api:install

Rails API Controller

ruby
# app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < ApplicationController
  before_action :set_user, only: [:show, :update, :destroy]

  # GET /api/v1/users
  def index
    @users = User.all
    render json: @users
  end

  # GET /api/v1/users/:id
  def show
    render json: @user
  end

  # POST /api/v1/users
  def create
    @user = User.new(user_params)
    
    if @user.save
      render json: @user, status: :created, location: @user
    else
      render json: @user.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /api/v1/users/:id
  def update
    if @user.update(user_params)
      render json: @user
    else
      render json: @user.errors, status: :unprocessable_entity
    end
  end

  # DELETE /api/v1/users/:id
  def destroy
    @user.destroy
  end

  private

  def set_user
    @user = User.find(params[:id])
  end

  def user_params
    params.require(:user).permit(:name, :email, :age)
  end
end

Rails Routes

ruby
# config/routes.rb
namespace :api do
  namespace :v1 do
    resources :users
  end
end

📡 Consuming Web Services

Using Net::HTTP

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

class APIClient
  BASE_URL = 'https://api.example.com'

  def initialize(base_url = BASE_URL)
    @base_url = base_url
  end

  def get(path)
    uri = URI.join(@base_url, path)
    response = Net::HTTP.get_response(uri)
    handle_response(response)
  end

  def post(path, data = {})
    uri = URI.join(@base_url, path)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = (uri.scheme == 'https')

    request = Net::HTTP::Post.new(uri)
    request['Content-Type'] = 'application/json'
    request.body = data.to_json

    response = http.request(request)
    handle_response(response)
  end

  private

  def handle_response(response)
    case response
    when Net::HTTPSuccess
      JSON.parse(response.body)
    when Net::HTTPNotFound
      raise "Resource not found"
    when Net::HTTPBadRequest
      raise "Bad request: #{response.body}"
    else
      raise "HTTP Error: #{response.code}"
    end
  end
end

# Usage
# client = APIClient.new('https://api.example.com')
# users = client.get('/users')
# result = client.post('/users', { name: 'John', email: 'john@example.com' })

Using HTTParty

ruby
# First install httparty
# gem install httparty

require 'httparty'

class GitHubAPI
  include HTTParty
  base_uri 'https://api.github.com'

  def initialize(token = nil)
    @options = {}
    @options[:headers] = { 'Accept' => 'application/vnd.github.v3+json' }
    @options[:headers]['Authorization'] = "token #{token}" if token
  end

  def get_user(username)
    self.class.get("/users/#{username}", @options)
  end

  def get_repos(username)
    self.class.get("/users/#{username}/repos", @options)
  end

  def create_repo(name, description = '', is_private = false)
    self.class.post('/user/repos',
      @options.merge(
        body: {
          name: name,
          description: description,
          private: is_private
        }.to_json
      )
    )
  end
end

# Usage
# github = GitHubAPI.new(ENV['GITHUB_TOKEN'])
# user = github.get_user('octocat')
# repos = github.get_repos('octocat')

📚 Next Steps

After mastering Ruby Web services, we recommend continuing to learn:

Continue your Ruby learning journey!

Content is for learning and research only.