SkillAgentSearch skills...

Brainstem

The Brainstem gem provides a framework for converting ActiveRecord objects into a great JSON API. Brainstem Presenters allow easy application of user-requested sorts, filters, and association loads, allowing for simpler implementations, fewer requests, and smaller responses.

Install / Use

/learn @mavenlink/Brainstem
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

If you're upgrading from an older version of Brainstem, please see Upgrading From The Pre 1.0 Brainstem and the rest of this README.

Brainstem

Gitter

Build Status

Brainstem is designed to power rich APIs in Rails. The Brainstem gem provides a presenter library that handles converting ActiveRecord objects into structured JSON and a set of API abstractions that allow users to request sorts, filters, and association loads, allowing for simpler implementations, fewer requests, and smaller responses.

Why Brainstem?

  • Separate business and presentation logic with Presenters.
  • Version your Presenters for consistency as your API evolves.
  • Expose end-user selectable filters and sorts.
  • Whitelist your existing scopes to act as API filters for your users.
  • Allow users to side-load multiple objects, with their associations, in a single request, reducing the number of requests needed to get the job done. This is especially helpful for building speedy mobile applications.
  • Prevent data duplication by pulling associations into top-level hashes, easily indexable by ID.
  • Easy integration with Backbone.js via brainstem-js. "It's like Ember Data for Backbone.js!"

Watch our talk about Brainstem from RailsConf 2013

Installation

Add this line to your application's Gemfile:

gem 'brainstem'

Usage

Make a Presenter

Create a class that inherits from Brainstem::Presenter, named after the model that you want to present, and preferrably versioned in a module. For example lib/api/v1/widget_presenter.rb:

module Api
  module V1
    class WidgetPresenter < Brainstem::Presenter
      presents Widget

      # Available sort orders to expose through the API
      sort_order :updated_at, "widgets.updated_at"
      sort_order :created_at, "widgets.created_at"

      # Default sort order to apply
      default_sort_order "updated_at:desc"

      # Optional filter that applies a lambda.
      filter :location_name, :string, items: [:sf, :la] do |scope, location_name|
        scope.joins(:locations).where("locations.name = ?", location_name)
      end

      # Filter with an overridable default. This will run on every request,
      # passing in `bool` as `false` unless a user has specified otherwise.
      filter :include_legacy_widgets, :boolean, default: false do |scope, bool|
        bool ? scope : scope.without_legacy_widgets
      end

      # The top-level JSON key in which these presented records will be returned.
      # This is optional and defaults to the model's table name.
      brainstem_key :widgets

      # Specify the fields to be present in the returned JSON.
      fields do
        field :name, :string,
              info: "the Widget's name"
        field :legacy, :boolean,
              info: "true for legacy Widgets, false otherwise",
              via: :legacy?
        field :longform_description, :string,
              info: "feature-length description of this Widget",
              optional: true
        field :aliases, :array,
              item_type: :string,
              info: "the differnt aliases for the widget"
        field :updated_at, :datetime,
              info: "the time of this Widget's last update"
        field :created_at, :datetime,
              info: "the time at which this Widget was created"

        # Fields can be nested under non-evaluable parent fields where the nested fields
        # are evaluated with the presented model.
        fields :permissions, :hash do |permissions_field|

          # Since the permissions parent field is not evaluable, the can_edit? method is
          # evaluated with the presented Widget model.
          permissions_field.field :can_edit, :boolean,
                                  via: :can_edit?,
                                  info: "Indicates if the user can edit the widget"
        end

        # Specify nested fields within an evaluable parent block field. A parent block field
        # is evaluable only if one of the following options :via, :dynamic or :lookup is specified.
        # The nested fields are evaluated with the value of the parent.
        fields :tags, :array,
               item_type: :hash,
               info: "The tags for the given category",
               dynamic: -> (widget) { widget.tags } do |tag|

          # The name method will be evaluated with each tag model returned by the the parent block.
          tag.field :name, :string,
                    info: "Name of the assigned tag"
        end

        fields :primary_category, :hash,
               via: :primary_category,
               info: "The primary category of the widget" do |category|

          # The title method will be evaluated with each category model returned by the parent block.
          category.field :title, :string,
                         info: "The title of the category"
        end
      end

      # Associations can be included by providing include=association_name in the URL.
      # IDs for belongs_to associations will be returned for free if they're native
      # columns on the model, otherwise the user must explicitly request associations
      # to avoid unnecessary loads.
      associations do
        association :features, Feature,
                    info: "features associated with this Widget"
        association :location, Location,
                    info: "the location of this Widget"
      end
    end
  end
end

Setup your Controller

Once you've created a presenter like the one above, pass requests through from your Controller.

class Api::WidgetsController < ActionController::Base
  include Brainstem::ControllerMethods

  def index
    render json: brainstem_present("widgets") { Widgets.visible_to(current_user) }
  end

  def show
    widget = Widget.find(params[:id])
    render json: brainstem_present_object(widget)
  end

  def create
    # Note: you are in charge of sanitizing params[brainstem_model_name], likely with strong parameters.
    widget = Widget.new(params[brainstem_model_name])
    if widget.save
      render json: brainstem_present_object(widget)
    else
      render json: brainstem_model_error(widget), status: :unprocessable_entity
    end
  end
end

The Brainstem::ControllerMethods concern provides:

  • brainstem_model_name which is inferred from your controller name or settable with self.brainstem_model_name = :thing.
  • brainstem_present and brainstem_present_object for presenting a scope of models or a single model.
  • brainstem_model_error and brainstem_system_error for presenting model and system error messages.
  • Various methods for auto-documentation of your API.

Controller Best Practices

We recommend that your base API controller look something like the following.

module Api
  module V1
    class ApiController < ApplicationController
      include Brainstem::ControllerMethods

      before_filter :api_authenticate

      rescue_from StandardError, with: :server_error
      rescue_from Brainstem::SearchUnavailableError, with: :search_unavailable
      rescue_from ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed
      rescue_from ActiveRecord::RecordNotFound,
                  ActionController::RoutingError, with: :page_not_found

      private

      def api_authenticate
        # Implement your authentication here.  We recommend Doorkeeper.
      end

      def server_error(exception)
        render json: brainstem_system_error("A server error has occurred."), status: 500
      end

      def search_unavailable
        render json: brainstem_system_error('Search is currently unavailable'), status: 503
      end

      def page_not_found
        render json: brainstem_system_error('Record not found'), status: 404
      end

      def record_not_destroyed
        render json: brainstem_model_error("Could not delete the #{brainstem_model_name.humanize.downcase.singularize}"), status: :unprocessable_entity
      end
    end
  end
end

Setup Rails to Load Brainstem

To configure Brainstem for development and production, we do the following:

  1. We add lib to our Rails autoload_paths in application.rb with config.autoload_paths += "#{config.root}/lib"

  2. We setup an initializer in config/initializers/brainstem.rb, similar to the following:

# In order to support live code reload in the development environment, we
# register a `to_prepare` callback. This # runs once in production (before the
# first request) and whenever a file has changed in development.
Rails.application.config.to_prepare do
  # Forget all Brainstem configuration.
  Brainstem.reset!

  # Set the current default API namespace.
  Brainstem.default_namespace = :v1

  # (Optional) Utilize MySQL's [FOUND_ROWS()](https://dev.mysql.com/doc/refman/5.7/en/information-functions.html#function_found-rows) 
  # functionality to avoid issuing a new query to calculate the record count, 
  # which has the potential to up to double the response time of the endpoint.
  Brainstem.mysql_use_calc_found_rows = true 

  # (Optional) Load a default base helper into all presenters. You could use
  # this to bring in a concept like `current_user`.  # While not necessarily the
  # best approach, something like http://stackoverflow.com/a/11670283 can
  # currently be used to # access the requesting user inside of a Brainstem
  # presenter. We hope to clean this up by allowing a user to be passed in #
  # when presenting in the future.
  module ApiHelper
    def current_user
      Thread.current[:current_user]
    end
  end

Related Skills

View on GitHub
GitHub Stars201
CategoryDevelopment
Updated7mo ago
Forks15

Languages

Ruby

Security Score

87/100

Audited on Aug 5, 2025

No findings