How to Quickly Add Foobara as a Service-Object Layer to an Existing Rails App

ยท Miles Georgi

Hey hey! As teams struggle to wrangle domain complexity in their Rails app, many solutions they reach for involve adding something between the controllers and the models to house their domain logic.

Bad things happen when integration code and domain logic get tangled up in projects with complex domains. Let's look at an example of this problem in a Rails app that has no such layer.

Setting up the Rails app

We'll quickly set up an existing Rails app with the following:

rails new --api greeter

Let's make a basic "Hello, World!" app. We'll just give it a greetings resource in config/routes.rb in fine Rails convention fashion:

Rails.application.routes.draw do resources :greetings, only: [:create], defaults: { format: :json } end

And now let's create a greetings controller with a create action in app/controllers/greetings_controller.rb:

class GreetingsController < ApplicationController def create salutation = (params[:salutation] || "hello").strip.capitalize if salutation =~ /\s*h*i*ss+\s*/i render json: { threatening_to_capybaras: { path: [:salutation], context: { threatening_salutation: salutation }, message: "Do not hiss around our capybaras!" } }, status: :unprocessable_entity return end greetee = (params[:greetee] || "world").strip.capitalize if salutation != "Hello" && greetee == "Universe" render json: { greeting_overreach: { context: { greetee: }, message: "Can we keep custom salutations within this galaxy, please?" } }, status: :unprocessable_entity return end render json: "#{salutation}, #{greetee}!" end end

Running the App

Let's test it out!

Fire up the rails app with rails s and in another terminal let's run:

$ curl -X POST localhost:3000/greetings ; echo
Hello, World!
$ curl -X POST localhost:3000/greetings?salutation=hi ; echo
Hi, World!
$ curl -X POST localhost:3000/greetings?salutation=hhhiiiissss ; echo
{"threatening_to_capybaras":{"path":["salutation"],"context":{"threatening_salutation":"Hhhiiiissss"},"message":"Do not hiss around our capybaras!"}}

The Problem

So what's wrong with this app? Well, if the domain complexity were high, I believe that coupling HTTP integration with domain logic in the controller action will lead to many headaches. Let's look at some of these separation of concerns issues:

(params[:salutation] || "hello").strip.capitalize

This might not seem like a violation of separation-of-concerns at first glance, but it is!

Digging inputs to the domain operation out of the body/query parameters is an HTTP-integration concern.

Defaulting the salutation to hello is a business logic concern. Capitalizing it is also a business logic concern.

Here's another example of this violation of separation-of-concerns:

if salutation =~ /\s*h*i*ss+\s*/i render json: { threatening_to_capybaras: { path: [:salutation], context: { threatening_salutation: salutation }, message: "Do not hiss around our capybaras!" } }, status: :unprocessable_entity return end

Here, preventing a "hiss"-adjacent salutation is a business logic concern.

But render json: and status: :unprocessable_entity are clearly HTTP-integration concerns.

And these aren't the only two spots where the concepts are coupled in this controller action.

Imagine if we wanted to use this domain operation in other contexts like via a Rake task, in an async job, or even via an MCP server. We'd have difficulty re-using this code because the domain-logic and HTTP-integration are so tightly coupled.

As mentioned, some teams make progress in this department by adding something between the controllers and the models to house their domain logic. This gives them a way to cleanly re-use domain logic in integration-agnostic ways.

A Solution

You can use Foobara to quickly fill the role of an intermediate-layer dedicated to domain operations. We can quickly set it up by using the foobify-rails-app generator. Let's install it!

Installing foobify-rails-app

One way to do this is gem install foobify-rails-app but what I like to do is add gem "foobify-rails-app" to my Gemfile and run bundle install. I prefer this method because it's easier for me to update it and keep track of it and not have it available where it's not relevant. So I'll go with that approach.

So, I'll add gem "foobify-rails-app" to my Gemfile and run bundle install:

group :development do gem "foobify-rails-app" end
$ bundle install
Fetching gem metadata from https://rubygems.org/..........
Resolving dependencies...
Fetching foobify-rails-app 0.0.12 (was 0.0.11)
Installing foobify-rails-app 0.0.12 (was 0.0.11)
Bundle updated!

Running foobify-rails-app

Pretty straightforward:

$ foobify-rails-app

All this does is add gem "foobara" to our Gemfile and adds a config/initializers/foobara.rb file to make sure Foobara behaves correctly whenever Zeitwerk does its thing.

It's really not much code at all to wire-up Foobara but not something worth memorizing so it is a handy script. And actually... it can do more. It can also give us a sample command to get started with!

Generating a Sample Command

$ foobify-rails-app --include-sample-command

This will generate a sample command called ConstructGreeting in app/commands/construct_greeting.rb.

Let's take a look at it!

class ConstructGreeting < Foobara::Command description "Takes an optional salutation and greetee to greet and constructs a greeting" inputs do salutation :string, default: "hello" greetee :string, default: "world" end result :string possible_input_error :salutation, :threatening_to_capybaras, context: { threatening_salutation: :string }, message: "Do not hiss around our capybaras!" possible_error :greeting_overreach, context: { greetee: :string }, message: "Can we keep custom salutations within this galaxy, please?" def execute normalize_greetee normalize_salutation check_for_greeting_overreach build_greeting greeting end def validate if salutation.strip =~ /\s*h*i*ss+\s*/i add_input_error :salutation, :threatening_to_capybaras, threatening_salutation: salutation end end attr_accessor :greeting, :normalized_salutation, :normalized_greetee def normalize_greetee self.normalized_greetee = greetee.strip.capitalize end def check_for_greeting_overreach if normalized_salutation != "Hello" && normalized_greetee == "Universe" add_runtime_error :greeting_overreach, greetee: end end def normalize_salutation self.normalized_salutation = salutation.strip.capitalize end def build_greeting self.greeting = "#{normalized_salutation}, #{normalized_greetee}!" end end

So aside from a good introduction to the anatomy of a Foobara command, this also contains no HTTP concerns at all!

Let's make use of it!

Calling a Command from a Controller

We will now simplify our controller significantly by just calling the command and wiring it up with Rails/HTTP:

class GreetingsController < ApplicationController def create outcome = ConstructGreeting.run(salutation: params[:salutation], greetee: params[:greetee]) if outcome.success? render json: outcome.result else render json: outcome.errors_hash, status: :unprocessable_entity end end end

While that simplified significantly, the most important thing is actually that there is no longer any domain logic whatsoever in our controller action! All of its code is concerned with integrating our high-level ConstructGreeting domain operation with the outside world via HTTP.

Conclusion

So there you have it... with a quick foobify-rails-app you can introduce a service-object layer between your rails controllers and your active record models. This could be a simple way to try out Foobara without needing to learn its more powerful features.

You can find the code for this demo app on GitHub and you can look through its git history if wanting to see the intermediate steps outlined in this blog post.

Foobara itself can be found at foobara.org or on GitHub.

Cheers!