A Guide to Trailblazer’s Operation

Everything to know about the essentials of an Operation

Ibrahim Areda
6 min readJan 3, 2024

Previously, we talked about Trailblazer as a whole in general, but today we’re going to dive deeper and explore one of its most powerful components; The Operation.

If you missed out on the previous story, you can find it here.

Introduction

We’re going to build a simple API for a Rock band catalog, managing it with simple CRUD actions, while explaining the basics of the operation along the way. So I need you to create a new Rails API project.

I am using Ruby 3.2.2 & Rails 7.1.1

Then, add the following gems to your Gemfile and run bundle install :

gem 'trailblazer'
gem 'trailblazer-rails'

Creating the model

Let’s get started and create our Band model:

rails g model Band name:string genre:string origin:string formed_in:integer

Edit the generated migration to alter the type of the id column to be uuid , and make genre an array of strings because a band is not limited to one genre, and let’s not forget to make all of these columns required… here’s the final form of the migration:

class CreateBands < ActiveRecord::Migration[7.1]
def change
create_table :bands, id: :uuid do |t|
t.string :name, null: false
t.string :genre, null: false, array: true
t.string :origin, null: false
t.date :formed_in, null: false

t.timestamps
end
end
end

Run rails db:migrate to create the database table for the Band model.

Creating the controller

Now that our model is ready, we’re lacking the controller to handle our CRUD actions, so we’ll run rails g controller Bands that will generate two files; a controller and its test file:

- app/controllers/bands_controller.rb
- test/controllers/bands_controller_test.rb

Then we’ll configure the routes to this controller, go ahead, and open up config/routes.rb — we’re only building index, showendpoints for this tutorial to save other modules for later so we can thoroughly explore Trailblazer and its components —
Our routes file should look like this:

Rails.application.routes.draw do
scope :api do
resources :bands, only: %i[index show]
end
end

Then we return to our controller file app/controllers/bands_controller.rb and define our index, show actions like this:

The index endpoint is expected to return an array of Band objects, but it’s bad practice to ignore REST API conventions and send out an array as a top-level response without proper wrapping; so we’ll be returning success property to indicate the success status of the endpoint, and data will contain our band objects.

class BandsController < ApplicationController
# GET /api/bands
def index
render json: {
success: true,
data: []
}, status: :ok
end

# GET /api/bands/:id
def show; end
end

Let’s run a quick test to check if everything is going smoothly before proceeding, so we’ll change our controller test file a bit, check if our controller is correctly routed, and assert that it returns success property with a true value.

rails t test/controllers/bands_controller_test.rb to run a test file.

require 'test_helper'

class BandsControllerTest < ActionDispatch::IntegrationTest
test 'GET #index' do
get '/api/bands'

assert_response :success
assert_predicate response.parsed_body['success'], :present?
assert response.parsed_body['success']
end
end

Creating the Index operation

So far, we’ve managed to prepare the stage to finally introduce the operations to our project, we’ll start with creating the Index , which should return the list of the bands stored in our database.

It’s best to put files related to Trailblazer under app/concepts ; They should be associated with their subject of attention, in our case, we’re going to nest them inside the directory app/concepts/band/operations because we’re handling the Band model. After creating index.rb file, we’ll make the class Index inherit from Trailblazer::Operation :

# app/concepts/band/operations/index.rb

module Band::Operations
class Index < Trailblazer::Operation
# TODO
end
end

Operations are Railway-oriented, meaning that its instruction blocks are distributed based on their responsibilities, those blocks are called steps, and are executed in a consecutive organized manner; The first step of our operation Index should be retrieving the list of the bands. And that’s about it, we’re not implementing pagination, and we’re not trying to add a filtering system either, that’s for another story that concentrates on the Trailblazer Finder module.

The context object is the most important tool in the operation, it allows communication between your operations and whatever else on the other side (such as the controller), and should always be present as the first argument in your step method arguments.

module Band::Operations
class Index < Trailblazer::Operation
step :set_entity

def set_entity(context, **)
context[:data] = Band.order(formed_in: :desc)
end
end
end

Now that we built Index , let’s return to our controller and utilize it in the index action, to invoke an operation, you can use run like this:

class BandsController < ApplicationController
# GET /api/bands
def index
run Band::Operations::Index do |context|
render json: {
success: true,
data: context[:data]
}, status: :ok
end
end

# GET /api/bands/:id
def show; end
end

The instructions inside the block with context argument will be executed when the operation follows the happy path. In this case, Operations::Index only has one step that assigns an array of bands to the context object, there is no other choice for it other than being happy.

To stray from the successful path, a step method should return false or nil, then the operation will either exits or execute the fail block if specified.

And whatever was passed to the context will be ready for us to use to our heart’s content! Giving us the possibility to render the sorted bands array we retrieved earlier.

Testing the Index endpoint

With that, the index endpoint should be done for now, let’s write a test for it to check if it delivers what is promised.

Before that, let’s mock some data in test/fixtures/bands.yml :

radiohead:
name: Radiohead
genre:
- Art Rock
- Alt Rock
origin: Oxfordshire, England
formed_in: 1995

green_day:
name: Green Day
genre:
- Pop Punk
- Punk Rock
origin: California, United States
formed_in: 1987

In our controller test, assert the number of bands we got when sending a request, and also check if it’s in the correct order (DESCending order of formed_in), then run the tests to make sure they pass:

require 'test_helper'

class BandsControllerTest < ActionDispatch::IntegrationTest
test 'GET #index' do
get '/api/bands'

assert_response :success
assert response.parsed_body['success']
assert_equal 2, response.parsed_body['data'].count
assert_equal 'Radiohead', response.parsed_body['data'][0]['name']
assert_equal 'Green Day', response.parsed_body['data'][1]['name']
end
end

Creating the Show Operation

This operation expects a path parameter id , to be able to search for the Band that holds that given ID, and it is expected to return a band if found.

With that in mind, we’ll create a step to find the model, only this time we need to get the id from the request’s parameters, thanks to Trailblazer, the parameters are passed from the controller to the operation when you invoke it with run , so you should be able to find it inside the context associated with the :params key, or you can choose the keyword-argument style to quickly access it from the method arguments instead like so:

# app/concepts/band/operations/show.rb

module Band::Operations
class Show < Trailblazer::Operation
step :find_model

def find_model(context, params:, **)
context[:model] = Band.find_by(id: params[:id])
end
end
end

Back to the controller, we’ll construct the showendpoint so that it signals us with 404 if the model is not found:

# app/controllers/bands_controller.rb

# GET /api/bands/:id
def show
run Band::Operations::Show do |context|
return render json: {
success: true,
data: context[:model]
}, status: :ok
end

render json: {
success: false,
error: 'The band is not found'
}, status: :not_found
end

Notice the usage of return inside the success block; as soon as the operation succeeds we want it to exit after rendering the model without resuming outside to do the failure path, because usually everything outside the success block tends to be error handling.

Testing the Show endpoint

To test the two possible outcomes of the show endpoint, let’s switch to our controller test, and add a new test to cover them, asserting the failure path where we send an invalid ID in the path parameters, and the happy path case where we do everything perfectly:

# test/controllers/bands_controller_test.rb

test 'GET #show' do
band = Band.find_by name: 'Green Day'

# FAILURE: not found
get '/api/bands/INVALID_ID'

assert_response :not_found
assert_not response.parsed_body['success']
assert_equal 'The band is not found', response.parsed_body['error']

# SUCCESS
get "/api/bands/#{band.id}"

assert_response :success
assert_equal 'Green Day', response.parsed_body['data']['name']
end

Conclusion

With that, we conclude the story for today, I tried to be as minimal as possible because it can be overwhelming when you merge all that Trailblazer can offer at once.

So I prefer dealing with each problem individually so we can give each subject the time it deserves, as a result, there will be more tutorials soon, so make sure to stick around if you are interested in the topic.

And finally, I hope I was concise, comprehensible, and helpful. Have a great day!

Related Stories

PART 1: Why you should use Trailblazer
PART 2: 🔹A Guide to Trailblazer’s Operation
PART 3: A Guide to Trailblazer’s Finder
PART 4: Everything you need to know about Trailblazer Representable

--

--

Ibrahim Areda

💎 A Rails Developer - I write about programming tutorials.