Introducing Blueprinter

Introducing Blueprinter

A new way to format and serialize APIs.

Blueprinter is a declarative and fast Ruby object to JSON serializer, an alternative to solutions like Active Model Serializers.

Why build another serializer?

When we began writing Blueprinter, existing options at the time of writing were few, most suffered from poor performance and hard to read code.

Procore has a very large API, with hundreds of endpoints representing hundreds more data structures. We needed a fast, simple option that we could use across our gamut of different needs. We noticed an opportunity to solve our needs as well as something we could offer that could fill a need in the Rails community.

Ok, so what does blueprinter offer?

The main difference that Blueprinter offers is the concept of views. A view in this context is a way to define multiple representations of an object for different uses. For example, in an API one often desires a different set of data coming back for an object via an index action vs a show action, and a very limited set of information when included as an association in other objects' endpoints. With most serializers, you would have to define a new serializer class for each different representation. Multiply this by about 500 models and you've got a real mess.

On top of this, we've committed to keeping Blueprinter clean, readable, easy to modify, simple, and extensible.

Using Blueprinter

So let’s talk about how you can actually implement Blueprinter on a rails project. In this tutorial, we will use Blueprinter for a Todo app built on Ruby on Rails.The tutorial will walk you through using Blueprinter to generate JSON for index and show endpoints.

Given you already have a Ruby on Rails app newly generated, you should now create the Todo model:

class Todo < ApplicationRecord
  validates :name, :description, presence: true
end

It has a corresponding migration that looks like this:

# create_todos
def change
  create_table :todos do |t|
    t.string :name
    t.text :description
    t.datetime :completed_at
    t.datetime :due_at

    t.timestamps
  end
end

Don't forget to run rake db:migrate.

Next, let's seed the database so that we have some samples to work with:

#db/seeds.rb
10.times do |num|
  Todo.create(name:        "todo#{num}",
              description: "The #{num} thing(s) we need to do",
              due_at:      Time.now + num.days)
end

Finally run rake db:seed to seed the database.

The API Endpoints with Blueprinter

Add Blueprinter to your gemfile.

# Gemfile
gem 'blueprinter'

Now run bundle install from your terminal to install the gem into your project.

We would want our index route to respond to GET /api/todos with a JSON containing an array of todos with limited fields:

[
  {
    "id":1,
    "completed_at":null,
    "due_at":"2018-03-01 23:09:53 UTC",
    "name":"todo0"
  },
  {
    "id":2,
    "completed_at":null,
    "due_at":"2018-03-02 23:09:53 UTC",
    "name":"todo1"
  },
  ...
]

As for our show route, we want it to respond to GET /api/todos/:id with a json containing a single todo object with extended fields:

{
  "id":1,
  "completed_at":null,
  "created_at":"2018-03-01 23:09:53 UTC",
  "description":"The 0 thing(s) we need to do",
  "due_at":"2018-03-01 23:09:53 UTC",
  "name":"todo0",
  "updated_at":"2018-03-01 23:09:53 UTC"
}

As you can see, the index route and the show route responds with the same type of object, Todo, however with different representations. The show route returning more fields (extended fields) than index. Blueprinter uses a concept called a view to set different representations for the same object. Let's jump into this by defining a TodoBlueprint class. Create a new file at app/blueprints/todo_blueprint.rb. The content should look like:

# app/blueprints/todo_blueprint.rb
class TodoBlueprint < Blueprinter::Base
  identifier :id
    
  view :normal do
    fields :name, :due_at, :completed_at
  end

  view :extended do
    include_view :normal
    fields :description, :created_at, :updated_at
  end
end

As you can see, the :normal view only contains three fields, where as the :extended view will include the fields from :normal as well as three additional fields. Additionally, all of the views will contain the :id field of the todo object.

Next we will create the TodosController with the index and show actions. The index actions will serialize a collection of Todo objects with Blueprinter by passing to TodoBlueprint.render the objects, and the :normal view. While the show action will render a single object using the :extended view.

# app/controllers/api/todos_controller.rb
module Api
  class TodosController < ApplicationController
    def index
      todos = TodoBlueprint.render Todo.all, view: :normal
      render json: todos
    end

    def show
      todo = TodoBlueprint.render Todo.find(params[:id]), view: :extended
      render json: todo
    end
  end
end

Finally, let's add todos to the route:

# config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    resources :todos, only: [:index, :show]
  end
end

Oj Support (Optional Step)

If you want Blueprinter to use OJ to generate JSON instead of the default JSON gem, you can do that.

In your Gemfile, add Oj and run bundle install:

# Gemfile
gem 'oj'

Finally, create an initializer file and add the following:

# config/initializers/blueprinter.rb
require 'oj'
Blueprinter.configure do |config|
  config.generator = Oj
end

Try it out

Ensure your server is started with rails s.

Navigate to localhost:3000/api/todos and you should now see the JSON response with a list of todos.

Next, navigate to localhost:3000/api/todos/1 and you should see the JSON response for a single object with the extended fields.

We welcome questions, discussions, and contributions on Blueprinter at the Github repo.