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
- SOAP Web Services: XML-based protocol using WSDL to describe services
- RESTful Web Services: HTTP-based protocol using JSON/XML for data transfer
- GraphQL: Modern API query language allowing clients to precisely specify needed data
🌐 Using Built-in Libraries to Create Web Services
Basic HTTP Server
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.startRESTful API Server
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:
# 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
endSinatra with Database
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
# Create new Rails API
rails new my_api --api
# Or add API mode to existing Rails app
# rails api:installRails API Controller
# 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
endRails Routes
# config/routes.rb
namespace :api do
namespace :v1 do
resources :users
end
end📡 Consuming Web Services
Using Net::HTTP
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
# 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:
- Ruby Database Access - Learn database operations
- Ruby Testing - Master testing practices
- Ruby Performance Optimization - Learn optimization techniques
Continue your Ruby learning journey!