A Guide to Trailblazer’s Representable

How to integrate Trailblazer decorators

Ibrahim Areda
5 min readJan 22, 2024

Welcome back to a brand new part of the Trailblazer series, this time we’re going to learn about Trailblazer’s approach to dealing with model decorators and representations.

In case you missed the previous stories:

PART 1: Why you should use Trailblazer
PART 2: A Guide to Trailblazer’s Operation
PART 3: A Guide to Trailblazer’s Finder

Table of contents:

1. What is a Representable?
2. Installation
3. Usage
4. Creating a representable for the Band model
5. Integrating the new decorators
6. Testing the new response format
7. Advanced #property options
8. Serializing a collection of objects
9. Conclusion

What is a Representable?

It is a decorator service that helps you define the desired structure of a model’s serialized result, allowing you to select which attributes to serialize/parse and even add custom properties.

For instance, let’s assume we have a model called User holding three fields id, email , password; Knowing that we have an admin controller that defines an index endpoint returning all users.

render json: User.all.to_json, status: :ok

# Actual result:
# [{ "id": 1, "email": "example@example.com", "password": "example123" }]

#to_json will serialize all attributes — even password — of User instances, causing a major password leak, now imagine if the user has an endpoint that returns other fellow users to follow! Absolute carnage.

Installation

Install representable gem and multi_json to be able to serialize data to JSON format.

# Gemfile

gem 'representable'
gem 'multi_json'

To create a representable, simply make the class inherit from Trailblazer::Decorator; and since we’re handling JSON data, we’ll need to include the JSON parser.

Still, in sync with the previous user example, I will create a representable to solve the password leak:

# app/concepts/user/representables/index.rb

module User::Representables
class Example < Representable::Decorator
include Representable::JSON

property :id
property :email
end
end

The method property points to an attribute that is desired to be serialized, I purposefully left out password to hide it and prevent it from appearing in the index endpoint response.

Usage

To benefit from a representable’s service:

# For serializing one single object
User::Representables::Example.new(User.all.first)

# For serializing a collection of objects
User::Representables::Example.for_collection.new(User.all)

When initializing a representable instance, it’s required to pass a model instance(s) to serialize.

#for_collection is mandatory if you need to serialize a collection.

This user example is not a part of the band catalog API tutorial, it’s just for demonstration purposes.

Creating a representable for the Band model

Picking up where we left off last time in the Band Catalog tutorial project, let’s create one representable for the index endpoint, and one for the show endpoint.

But first, we’ll create an abstract representable so that we don’t have to repeat including Representable::JSON every time we create a new decorator.

# app/concepts/application_representable.rb

class ApplicationRepresentable < Representable::Decorator
include Representable::JSON
end

Then we create an index representable and make it inherit from the newly created abstract class.

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

module Band::Representables
class Index < ApplicationRepresentable
property :id
property :name
property :genre
end
end

Do the same for the show class, but this time let’s include more attributes:

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

module Band::Representables
class Show < ApplicationRepresentable
property :id
property :name
property :genre
property :origin
property :formed_in
end
end

Integrating the new decorators

Let’s edit bands_controller.rb to make index and show actions use the representable decorators:

# app/controllers/bands_controller.rb

class BandsController < ApplicationController
# GET /api/bands
def index
run Band::Operations::Index do |context|
result = context[:finder].result

render json: {
success: true,
data: Band::Representables::Index.for_collection.new(result),
meta: {
current_page: result.current_page,
next_page: result.next_page,
prev_page: result.prev_page,
total_pages: result.total_pages
}
}, status: :ok
end
end

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

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

Notice that meta pagination-related properties can be escorted to an independent decorator, which can be reused in other endpoints that might feature pagination in the future.

Create a new file:

# app/concepts/pagination_representable.rb

class PaginationRepresentable < ApplicationRepresentable
property :current_page
property :next_page
property :prev_page
property :total_pages
end

Replace the meta object with an instance of PaginationRepresentable:

# app/controllers/bands_controller.rb

# GET /api/bands
def index
run Band::Operations::Index do |context|
result = context[:finder].result

render json: {
success: true,
data: Band::Representables::Index.for_collection.new(result),
meta: PaginationRepresentable.new(result)
}, status: :ok
end
end

Testing the new response format

Next to test/controllers/bands_controller_test.rb , add a new assertion to GET #index test block that we wrote before; we’ll check if the response respects the desired structure by checking the returned keys in data objects:

# test/controllers/bands_controller_test.rb

test 'GET #index' do
...

assert_equal %w[id name genre], response.parsed_body['data'][0].keys
end

Do the same for GET #show test:

# test/controllers/bands_controller_test.rb

test 'GET #show' do
...

assert_equal %w[id name genre origin formed_in], response.parsed_body['data'].keys
end

Advanced #property options

The Representable::Decorator#property class gives us many powerful tools to let us fully control and manipulate the serialization process of the object properties:

  • as: Change the key name at serialization time
# Before adding `as:`
# { formed_in: 1999 }

property :formed_in, as: :formation_year

# Afer adding `as:`
# { formation_year: 1999 }
  • if: Conditional serialization
# Will only serialize this property if its value is bigger than 1990

property :formed_in, if: ->(*) { formed_in > 1990 }
  • getter: Overrides default serialized value
# Ignores the actual value and returns whatever you specify in `getter:`

property :formed_in, getter: ->(*) { 2000 }
  • render_nil: Ability to serialize nil values — default value is false
# Even if formed_in attribute is null, it will serialize it & display it

property :formed_in, render_nil: true
  • decorator: Either use a representable for a property’s serialization or use a custom method.
# decorator: Representable
property :band, decorator: Band::Representables::Show

# decorator: :exec_context
property :genre, decorator: :exec_context

def genre
'overriding the serialized value of genre'
end

Serializing a collection of objects

Let’s assume that we have another model called Album, which joins our Band model in a One-to-Many association where Band is the parent.

Knowing that the association is declared in Band, then supposedly we have access to a band’s discography.

# app/models/band.rb

has_many :albums

To be able to serialize a collection of records, we use the method #collection with either defining a block to specify which attributes to serialize or simply using the decorator: argument to select which representable to use:

collection :albums do
property :id
property :cover
property :name
property :year
end

# decorator:
collection :albums, decorator: Album::Representables::Show

Conclusion

We’ll surely get the chance to implement and discover more about the representable soon enough.

As you noticed, we are still missing a couple of CRUD actions, notably the #create endpoint, so next time we’ll be focusing on creating and updating band instances.

With the help of another extraordinary Trailblazer module which is the Reform whose responsibility is validating incoming input to prevent chaos.

I hope this was helpful to you, see you next time 😄

--

--

Ibrahim Areda

💎 A Rails Developer - I write about programming tutorials.