Acfs - API client for services

Gem Version Build Status GitHub Workflow Status Coverage Status RubyDoc Documentation

Acfs is a library to develop API client libraries for single services within a larger service oriented application.

Acfs covers model and service abstraction, convenient query and filter methods, full middleware stack for pre-processing requests and responses, as well as automatic request queuing and parallel processing.


Add this line to your application's Gemfile:

gem 'acfs', '~> 1.7'

And then execute:

> bundle

Or install it yourself as:

> gem install acfs


First you need to define your service(s):

class UserService < Acfs::Service
  self.base_url = ''

  # You can configure middlewares you want to use for the service here.
  # Each service has it own middleware stack.
  use Acfs::Middleware::JsonDecoder
  use Acfs::Middleware::MessagePackDecoder

This specifies where the UserService is located. You can now create some models representing resources served by the UserService.

class User < Acfs::Resource
  service UserService # Associate `User` model with `UserService`.

  # Define model attributes and types
  # Types are needed to parse and generate request and response payload.

  attribute :id, :uuid # Types can be classes or symbols.
                       # Symbols will be used to load a class from `Acfs::Model::Attributes` namespace.
                       # Eg. `:uuid` will load class `Acfs::Model::Attributes::Uuid`.

  attribute :name, :string, default: 'Anonymous'
  attribute :age, ::Acfs::Model::Attributes::Integer # Or use :integer


The service and model classes can be shipped as a gem or git submodule to be included by the frontend application(s).

You can use the model there:

@user = User.find 14

@user.loaded? #=> false # This will run all queued request as parallel as possible.
         # For @user the following URL will be requested:
         # `` # => "..."

@users = User.all
@users.loaded? #=> false # Will request ``

@users #=> [<User>, ...]

If you need multiple resources or dependent resources first define a "plan" how they can be loaded:

@user = User.find(5) do |user|
  # Block will be executed right after user with id 5 is loaded

  # You can load additional resources also from other services
  # Eg. fetch comments from `CommentSerivce`. The line below will
  # load comments from ``
  @comments = Comment.where user:

  # You can load multiple resources in parallel if you have multiple
  # ids.
  @friends  = User.find 1, 4, 10 do |friends|
    # This block will be executed when all friends are loaded.
    # [ ... ]
end # This call will fire all request as parallel as possible.
         # The sequence above would look similar to:
         # Start                Fin
         #   |===================|       ``
         #   |====|                      /users/5
         #   |    |==============|       /comments?user=5
         #   |    |======|               /users/1
         #   |    |=======|              /users/4
         #   |    |======|               /users/10

# Now we can access all resources:       # => "John
@comments.size   # => 25
@friends[0].name # => "Miraculix"

Use .find_by to get first element only. .find_by will call the index-Action and return the first resource. Optionally passed parameters will be sent as GET parameters and can be used for filtering in the service's controller.

@user = User.find_by age: 24 # Will request ``

@user # Contains the first user object returned by the index action

If no object can be found, .find_by will return nil. The optional callback will then be called with nil as parameter. Use .find_by! to raise an Acfs::ResourceNotFound exception if no object can be found. .find_by! will only invoke the optional callback if an object was successfully loaded.

Acfs has basic update support using PUT requests:

@user = User.find 5 = "Bob"

@user.changed? # => true
@user.persisted? # => false # Or .save!
           # Will PUT new resource to service synchronously.

@user.changed? # => false
@user.persisted? # => true

Singleton resources

Singletons can be used in Acfs by creating a new resource which inherits from SingletonResource:

class Single < Acfs::SingletonResource
  service UserService # Associate `Single` model with `UserService`.

  # Define model attributes and types as with regular resources

  attribute :name, :string, default: 'Anonymous'
  attribute :age, :integer


The following code explains the routing for singleton resource requests:

my_single = # sends POST request to /single

my_single = Single.find # sends GET request to /single

my_single.age = 28 # sends PUT request to /single

my_single.delete # sends DELETE request to /single

You also can pass parameters to the find call. They will be sent as query parameters to the index action:

my_single = Single.find name: 'Max' # sends GET request with param to /single?name=Max

Resource Inheritance

Acfs provides a resource inheritance similar to ActiveRecord Single Table Inheritance. If a type attribute exists and is a valid subclass of your resource they will be converted to you subclassed resources:

class Computer < Acfs::Resource

class Pc < Computer end
class Mac < Computer end

With the following response on GET /computers the collection will contain the appropriate subclass resources:

    { "id": 5, "type": "Computer"},
    { "id": 6, "type": "Mac"},
    { "id": 8, "type": "Pc"}
@computers = Computer.all

@computer[0].class # => Computer
@computer[1].class # => Mac
@computer[2].class # => Pc


You can stub resources in applications using an Acfs service client:

# spec_helper.rb

# This will enable stabs before each spec and clear internal state
# after each spec.
require 'acfs/rspec'
before do
  @stub = Acfs::Stub.resource MyUser, :read, with: { id: 1 }, return: { id: 1, name: 'John Smith', age: 32 }
  Acfs::Stub.resource MyUser, :read, with: { id: 2 }, raise: :not_found
  Acfs::Stub.resource Session, :create, with: { ident: '', password: 's3cr3t' }, return: { id: 'longhash', user: 1 }
  Acfs::Stub.resource MyUser, :update, with: lambda { |op| :my_var }, raise: 400

it 'should find user number one' do
  user = MyUser.find 1

  expect( eq 1
  expect( eq 'John Smith'
  expect(user.age).to eq 32

  expect(@stub).to be_called
  expect(@stub).to_not be_called 5.times

it 'should not find user number two' do
  MyUser.find 3

  expect { }.to raise_error(Acfs::ResourceNotFound)

it 'should allow stub resource creation' do
  session = Session.create! ident: '', password: 's3cr3t'

  expect( eq 'longhash'
  expect(session.user).to eq 1

By default Acfs raises an error when a non stubbed resource should be requested. You can switch of the behavior:

before do
  Acfs::Stub.allow_requests = true

it 'should find user number one' do
  user = MyUser.find 1             # Would have raised Acfs::RealRequestNotAllowedError
                       # Will run real request to user service instead.


Acfs supports instrumentation via active support and exposes the following events:

  • acfs.operation.complete(operation, response): Acfs operation completed
  • acfs.runner.sync_run(operation): Run operation right now skipping queue.
  • acfs.runner.enqueue(operation): Enqueue operation to be run later.
  • acfs.before_run: directly before
  • Run all queued operations.

Read the official guide on how to subscribe to these events.


  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Add specs for your feature
  4. Implement your feature
  5. Commit your changes (git commit -am 'Add some feature')
  6. Push to the branch (git push origin my-new-feature)
  7. Create new Pull Request


MIT License

Copyright (c) 2013-2022 Jan Graichen. MIT license, see LICENSE for more details.