A Ruby gem that provides a simple and elegant way to create API clients through configuration. It allows you to define API endpoints and their behavior declaratively, making it easy to create and maintain API clients.
- Declarative API client configuration
- Support for different request body formats (JSON, query params)
- Customizable headers
- Nested routing support
- ActiveSupport integration for logging and notifications
- Error handling with detailed response information
- Flexible parameter handling
- Automatic HTTP method detection based on method names
- Streaming support for handling large payloads
- Built-in debugging capabilities
Add this line to your application's Gemfile:
gem 'client-api-builder'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install client-api-builder
Create an API client by including the ClientApiBuilder::Router
module and defining your endpoints:
class MyApiClient
include ClientApiBuilder::Router
# Set the base URL for all requests
base_url 'https://api.example.com'
# Set default headers
header 'Content-Type', 'application/json'
header 'Accept', 'application/json'
# Define an endpoint
route :get_user, '/users/:id', method: :get, expected_response_code: 200
route :create_user, '/users', method: :post, expected_response_code: 201, body: { name: :name, email: :email }
end
# Use the client
client = MyApiClient.new
user = client.get_user(id: 123)
new_user = client.create_user(name: 'John', email: '[email protected]')
The Router automatically detects the HTTP method based on the method name if not explicitly specified. This makes your API client code more intuitive and reduces boilerplate. The detection rules are:
- Methods starting with
post
,create
,add
, orinsert
→POST
- Methods starting with
put
,update
,modify
, orchange
→PUT
- Methods starting with
patch
→PATCH
- Methods starting with
delete
orremove
→DELETE
- All other methods →
GET
Example:
class MyApiClient
include ClientApiBuilder::Router
base_url 'https://api.example.com'
# These will automatically use the appropriate HTTP methods
route :get_users, '/users' # Uses GET
route :create_user, '/users', body: { name: :name } # Uses POST
route :update_user, '/users/:id', body: { name: :name } # Uses PUT
route :delete_user, '/users/:id' # Uses DELETE
# You can still explicitly specify the method if needed
route :custom_action, '/custom', method: :post
end
By default, the client converts the body data to JSON. To use query parameters instead:
class MyApiClient
include ClientApiBuilder::Router
# Use query parameters for the body
body_builder :query_params
base_url 'https://api.example.com'
route :search, '/search', body: { q: :query, page: :page }
end
For APIs with nested resources, you can use the NestedRouter
:
class MyApiClient
include ClientApiBuilder::Router
base_url 'https://api.example.com'
section :users do
route :list, '/', method: :get
route :get, '/:id', method: :get
end
end
client = MyApiClient.new
users = client.users.list
user = client.users.get(id: 123)
The gem provides custom error classes for better error handling:
begin
client.get_user(id: 123)
rescue ClientApiBuilder::UnexpectedResponse => e
puts "Request failed with status #{e.response.status}"
puts "Response body: #{e.response.body}"
end
The gem integrates with ActiveSupport for logging and notifications:
# Enable logging
ClientApiBuilder.logger = Logger.new(STDOUT)
# Subscribe to notifications
ActiveSupport::Notifications.subscribe('request.client_api_builder') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
puts "Request took #{event.duration}ms"
end
The library supports streaming responses, which is particularly useful for handling large payloads. You can stream responses directly to a file or process them in chunks:
class MyApiClient
include ClientApiBuilder::Router
base_url 'https://api.example.com'
# Stream response directly to a file
route :download_users, '/users', stream: :file
# Stream response in chunks for custom processing
route :stream_users, '/users', stream: true
end
# Use the client
client = MyApiClient.new
# Stream to file
client.download_users('users.json') # Saves response directly to users.json
# Stream with custom processing
client.stream_users do |chunk|
# Process each chunk of the response
puts "Received #{chunk.bytesize} bytes"
end
When using stream: :file
, the response is written directly to disk as it's received, which is memory-efficient for large responses. The file path is passed as an argument to the method.
For custom streaming, you can provide a block that will be called with each chunk of the response as it's received. This allows for custom processing of large responses without loading the entire response into memory.
The library is not thread-safe. You must create a separate client instance per thread to avoid race conditions and ensure proper operation:
# Correct: Create a new client for each thread
threads = []
5.times do |i|
threads << Thread.new do
client = MyApiClient.new
client.get_user(id: i)
end
end
threads.each(&:join)
# Incorrect: Do not share a single client across threads
client = MyApiClient.new
threads = []
5.times do |i|
threads << Thread.new do
client.get_user(id: i) # This will cause race conditions
end
end
threads.each(&:join)
Each client instance maintains its own state, including response objects and request options. Sharing a client across threads can lead to unpredictable behavior and race conditions.
The Router provides built-in debugging capabilities by maintaining the response object and request options for each request. This information can be accessed after making a request:
client = MyApiClient.new
client.get_user(id: 123)
# Access the last response
response = client.response
puts "Status: #{response.status}"
puts "Headers: #{response.headers}"
puts "Body: #{response.body}"
# Access the request options used
request_options = client.request_options
puts "Method: #{request_options[:method]}"
puts "URL: #{request_options[:url]}"
puts "Headers: #{request_options[:headers]}"
puts "Body: #{request_options[:body]}"
# Access timing information
puts "Total request time: #{client.total_request_time} seconds"
puts "Number of attempts: #{client.request_attempts}"
This information is particularly useful for:
- Debugging failed requests
- Understanding request/response cycles
- Performance analysis
- Logging and monitoring
The response and request options are maintained until the next request is made, so you can inspect them immediately after a request completes.
base_url
: Set the base URL for all requestsheader
: Add headers to all requestsbody_builder
: Configure how request bodies are formattedroute
: Define API endpoints with their paths and parameterssection
: Define nested resource routes
Bug reports and pull requests are welcome on GitHub at https://github.com/dougyouch/client-api-builder.
The gem is available as open source under the terms of the MIT License.