A single Rails API endpoint to accept all changes to the app state

… and check why 5600+ Rails engineers read also this

A single Rails API endpoint to accept all changes to the app state

This idea is heavily influenced by CQRS and its way of applying changes to the app via commands objects. In this blogpost we’re showing how it could work with Rails.

Commands are data structures which represent the intention of the user. In the Rails community, we sometimes use the name of “a form object” to represent the same meaning.

In some of our projects, we started moving to a command-driven approach. A command is handled by a command handler (often it’s a service object). As a result of handling the command we publish domain events.

When you switch to commands, you’ll notice that many controllers look alike and they’re becoming a boiler-plate code which you repeat over and over.

This is what led to a conversation between me and Paweł. We discussed whether it makes sense to have just one controller, being represented by just one API endpoint.

Paweł decided to experiment with this idea and wrote the code below. This code is also a nice example of how concise can be a one-file-Rails application.


require 'action_controller/railtie'
require 'securerandom'

module Command
  Error = Class.new(StandardError)
end

module Service
  Error = Class.new(StandardError)
end

FooBarCommand = Class.new(OpenStruct) do
  ValidationError = Class.new(Command::Error)
  def validate!
    raise ValidationError if [foo, bar].any? { |value| value.nil? || value == '' }
  end
end

class FooBarService
  FooNotFooError = Class.new(Service::Error)
  def call(cmd)
    raise FooNotFooError unless cmd.foo == 'foo'
  end
end

COMMANDS =
  { 'foo_bar' => FooBarCommand,
  }

HANDLERS =
  { FooBarCommand => FooBarService.new,
  }

class Dummy < ::Rails::Application
  config.eager_load = false
  config.secret_key_base = SecureRandom.hex
end

class CommandsController < ActionController::Base
  def create
    cmd =
      COMMANDS
        .fetch(params[:command])
        .new(params.except(:command))
    cmd.validate!
    HANDLERS
      .fetch(cmd.class)
      .call(cmd)
    head :no_content
  rescue KeyError
    render json: {}, status: :not_found
  rescue Command::Error
    render json: {}, status: :unprocessable_entity
  rescue Service::Error
    render json: {}, status: :unprocessable_entity
  end
end

Dummy.initialize!
Dummy.routes.draw do
  resources :commands, only: :create
end
Dummy.routes.default_url_options[:host] = 'dummy.org'

run Rails.application

# How to run this example:
#
# rackup
# http POST localhost:9292/commands command=foo_bar foo=foo bar=bar

I really like this concept. I think it has the potential of removing a lot of controller code.

If this can work in some cases, this idea would become the most radical one in my book on dealing with Rails controllers. In the current version of the book, we talk a lot about the concept of form objects as a data structure which is initialized in the controller and passed to the service object.

The approach with a generic controller handling commands removes a big part of the controller layer.

Knowing Paweł, there will be updates and improvements to this approach, so stay tuned :)

You might also like