A Guide to Trailblazer’s Operation
Everything to know about the essentials of an Operation
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, show
endpoints 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
ornil
, then the operation will either exits or execute thefail
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 show
endpoint 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