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/BrainstemREADME
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
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_namewhich is inferred from your controller name or settable withself.brainstem_model_name = :thing.brainstem_presentandbrainstem_present_objectfor presenting a scope of models or a single model.brainstem_model_errorandbrainstem_system_errorfor 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:
-
We add
libto our Rails autoload_paths in application.rb withconfig.autoload_paths += "#{config.root}/lib" -
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
node-connect
343.3kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
92.1kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
343.3kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
343.3kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
