Share
Brainstem, your companion for rich Rails APIs
This talk will introduce Brainstem, a new Rails library for easily presenting and versioning complex ActiveRecord model relationships through your JSON API. Allow your internal or external API consumers to eager-load model associations, request custom scopes and sorts, load multiple objects by ID simultaneously, and generate JSON that uses references instead of repeating data.
While your Brainstem API can be consumed by any JSON client, it will truly shine when using the included Backbone integration, adding relationship-aware models, centralized data loading, and a smart caching identity map to your Backbone applications.
All of this is designed to reduce network requests and simplify development of HTML5 applications, especially mobile ones. With Backbone + Brainstem, loading a hierarchy of objects from your server can be reduced to one line of code and one network request.
This talk will survey Brainstem usage in Rails, then dive into how it can enable rich mobile HTML5 applications.
Andrew Cantino
Transcript
86 slides
-
This Talk • What do you want in an API?
• Why we built Brainstem • Presenters and Associations • Filters and Sorts • Brainstem.js • Decisions and Challenges • Other Libraries -
This Talk • What do you want in an API?
• Why we built Brainstem • Presenters and Associations • Filters and Sorts • Brainstem.js • Decisions and Challenges • Other Libraries -
You want an API for your users’ users for your
users you know... -
You want an API for your users’ users for your
users for yourselves you know... -
a fast API Is close to the database Side-loads associations
in a single request (no N+1) -
a fast API Is close to the database Side-loads associations
in a single request (no N+1) Avoids object repetition -
a fast API Is close to the database Side-loads associations
in a single request (no N+1) Avoids object repetition Allows for expressive filtering to avoid excess loads -
This Talk • What do you want in an API?
• Why we built Brainstem • Presenters and Associations • Filters and Sorts • Brainstem.js • Decisions and Challenges • Other Libraries -
{ "project_title": "RailsConf 2013", "private": true, "recipients": ["Jeff Moore", "Roger
Neel", "You"], "text": "Hey guys, I'm working on the RailsConf 2013 slides now.", "linked_task": "Make presentation", "attachments": [ { "title": "RailsConf 2013 Talk", "type": "key", "size": "240.33KB" } ], "replies": [ { "from": "Jeff Moore", "text": "Looks like a great start! :thumbsup:", } ] } Option One -
{ "project_title": "RailsConf 2013", "private": true, "recipients": ["Jeff Moore", "Roger
Neel", "You"], "text": "Hey guys, I'm working on the RailsConf 2013 slides now.", "linked_task": "Make presentation", "attachments": [ { "title": "RailsConf 2013 Talk", "type": "key", "size": "240.33KB" } ], "replies": [ { "from": "Jeff Moore", "text": "Looks like a great start! :thumbsup:", } ] } That’s great, but... Option One -
{ "project_title": "RailsConf 2013", "private": true, "recipients": ["Jeff Moore", "Roger
Neel", "You"], "text": "Hey guys, I'm working on the RailsConf 2013 slides now.", "linked_task": "Make presentation", "attachments": [ { "title": "RailsConf 2013 Talk", "type": "key", "size": "240.33KB" } ], "replies": [ { "from": "Jeff Moore", "text": "Looks like a great start! :thumbsup:", } ] } • What if I want other information too? That’s great, but... Option One -
{ "project_title": "RailsConf 2013", "private": true, "recipients": ["Jeff Moore", "Roger
Neel", "You"], "text": "Hey guys, I'm working on the RailsConf 2013 slides now.", "linked_task": "Make presentation", "attachments": [ { "title": "RailsConf 2013 Talk", "type": "key", "size": "240.33KB" } ], "replies": [ { "from": "Jeff Moore", "text": "Looks like a great start! :thumbsup:", } ] } • What if I want other information too? •What if I want to use this somewhere else? That’s great, but... Option One -
{ "project_title": "RailsConf 2013", "private": true, "recipients": ["Jeff Moore", "Roger
Neel", "You"], "text": "Hey guys, I'm working on the RailsConf 2013 slides now.", "linked_task": "Make presentation", "attachments": [ { "title": "RailsConf 2013 Talk", "type": "key", "size": "240.33KB" } ], "replies": [ { "from": "Jeff Moore", "text": "Looks like a great start! :thumbsup:", } ] } • What if I want other information too? •What if I want to use this somewhere else? • What if I’m requesting lots of Posts at once? That’s great, but... Option One -
{ "project_title": "RailsConf 2013", "private": true, "recipients": ["Jeff Moore", "Roger
Neel", "You"], "text": "Hey guys, I'm working on the RailsConf 2013 slides now.", "linked_task": "Make presentation", "attachments": [ { "title": "RailsConf 2013 Talk", "type": "key", "size": "240.33KB" } ], "replies": [ { "from": "Jeff Moore", "text": "Looks like a great start! :thumbsup:", } ] } • What if I want other information too? •What if I want to use this somewhere else? • What if I’m requesting lots of Posts at once? • And where did my awesome relational model go? That’s great, but... Option One -
{ "posts": { "25": { "project_id": "5", "private": true, "user_id":
"45", "recipient_ids": ["1", "7", "45"], "text": "Hey guys, I'm working on RailsConf 2013 slides now.", "task_id": "7", "attachment_ids": ["1"], "reply_ids": ["9"] } } } Option Two -
{ "posts": { "25": { "project_id": "5", "private": true, "user_id":
"45", "recipient_ids": ["1", "7", "45"], "text": "Hey guys, I'm working on RailsConf 2013 slides now.", "task_id": "7", "attachment_ids": ["1"], "reply_ids": ["9"] }, "9": { "user_id": "7", "project_id": "5", "parent_id": "25", "private": true, "text": "Looks like a great start! :thumbsup:" } } } Option Two -
{ "projects": { "5": { "title": "RailsConf 2013", ... }
}, "users": { "1": { "name": "Roger Neel", ... }, "7": { "name": "Jeff Moore", ... }, "45": { "name": "Andrew Cantino", ... } }, "tasks": { "7": { "title": "Make presentation", ... } }, "attachments": { "1": { "title": "RailsConf 2013 Talk", "type": "key", "size": "240.33KB", ... } } } Option Two -
{ "projects": { "5": { "title": "RailsConf 2013", ... }
}, "users": { "1": { "name": "Roger Neel", ... }, "7": { "name": "Jeff Moore", ... }, "45": { "name": "Andrew Cantino", ... } }, "tasks": { "7": { "title": "Make presentation", ... } }, "attachments": { "1": { "title": "RailsConf 2013 Talk", "type": "key", "size": "240.33KB", ... } } } Option Two -
{ "projects": { "5": { "title": "RailsConf 2013", ... }
}, "users": { "1": { "name": "Roger Neel", ... }, "7": { "name": "Jeff Moore", ... }, "45": { "name": "Andrew Cantino", ... } }, "tasks": { "7": { "title": "Make presentation", ... } }, "attachments": { "1": { "title": "RailsConf 2013 Talk", "type": "key", "size": "240.33KB", ... } } } Option Two • It seems longer, but it’s more reusable. -
{ "projects": { "5": { "title": "RailsConf 2013", ... }
}, "users": { "1": { "name": "Roger Neel", ... }, "7": { "name": "Jeff Moore", ... }, "45": { "name": "Andrew Cantino", ... } }, "tasks": { "7": { "title": "Make presentation", ... } }, "attachments": { "1": { "title": "RailsConf 2013 Talk", "type": "key", "size": "240.33KB", ... } } } Option Two • It seems longer, but it’s more reusable. • It will end up being more consistent. -
{ "projects": { "5": { "title": "RailsConf 2013", ... }
}, "users": { "1": { "name": "Roger Neel", ... }, "7": { "name": "Jeff Moore", ... }, "45": { "name": "Andrew Cantino", ... } }, "tasks": { "7": { "title": "Make presentation", ... } }, "attachments": { "1": { "title": "RailsConf 2013 Talk", "type": "key", "size": "240.33KB", ... } } } Option Two • It seems longer, but it’s more reusable. • It will end up being more consistent. • We got our awesome relational model back! -
{ "projects": { "5": { "title": "RailsConf 2013", ... }
}, "users": { "1": { "name": "Roger Neel", ... }, "7": { "name": "Jeff Moore", ... }, "45": { "name": "Andrew Cantino", ... } }, "tasks": { "7": { "title": "Make presentation", ... } }, "attachments": { "1": { "title": "RailsConf 2013 Talk", "type": "key", "size": "240.33KB", ... } } } Option Two • It seems longer, but it’s more reusable. • It will end up being more consistent. • We got our awesome relational model back! •And we get it all in one request! /api/v1/posts/25.json? include=project,user,recipients,task,attachments,replies -
This Talk • What do you want in an API?
• Why we built Brainstem • Presenters and Associations • Filters and Sorts • Brainstem.js • Decisions and Challenges • Other Libraries -
module Api module V1 class PostPresenter < Brainstem::Presenter presents "Post"
def present(post) { :text => post.text, :private => post.private?, :created_at => post.created_at } end end end end Presenters -
def present(post) { :text => post.text, :private => post.private?, :created_at
=> post.created_at, :replies => association(:replies), :task => association(:task) :attachments => association(:attachments) :user => association(:user) :recipients => association(:recipients, :json_name => "users"), :project => association(:project) } end Presenters with associations -
module Api module V1 class UserPresenter < Brainstem::Presenter presents "User"
def present(user) { :name => user.name, :email => user.email, :posts => association(:posts) } end end end end Presenters with associations -
module Api module V1 class PostsController < ApplicationController include Brainstem::ControllerMethods
def index render :json => present("posts") { # Post.relavent_for_api # Post.visible_to(current_user) Post.unscoped } end Presenters from Rails /api/v1/posts.json?include=user,... /api/v1/posts.json?only=5,8,25 /api/v1/posts.json?per_page=20&page=2 -
This Talk • What do you want in an API?
• Why we built Brainstem • Presenters and Associations • Filters and Sorts • Brainstem.js • Decisions and Challenges • Other Libraries -
module Api module V1 class PostPresenter < Brainstem::Presenter presents "Post"
sort_order :updated_at, "updated_at" sort_order :created_at, "created_at" default_sort_order "updated_at:desc" Sorting /api/v1/posts.json?order=created_at:asc -
module Api module V1 class PostPresenter < Brainstem::Presenter presents "Post"
filter :task_id do |scope, task_id| scope.where(:task_id => task_id.to_i) end filter :popular Filtering with lambdas /api/v1/posts.json?task_id=5 /api/v1/posts.json?popular=true -
module Api module V1 class PostPresenter < Brainstem::Presenter presents "Post"
filter :include_private_posts, :default => true do |scope, bool| bool ? scope : scope.only_public_posts end Filtering with defaults /api/v1/posts.json /api/v1/posts.json?include_private_posts=false -
This Talk • What do you want in an API?
• Why we built Brainstem • Presenters and Associations • Filters and Sorts • Brainstem.js • Decisions and Challenges • Other Libraries -
@data = new Brainstem.StorageManager() @data.addCollection 'posts', Collections.Posts @data.addCollection 'projects', Collections.Projects
@data.addCollection 'users', Collections.Users Brainstem.StorageManager The StorageManager is an object identity map and data loader for Backbone. -
class Models.Post extends Brainstem.Model paramRoot: 'post' brainstemKey: 'posts' urlRoot: '/api/v1/posts'
@associations: project: "projects" replies: ["posts"] user: "users" recipients: ["users"] task: "tasks" attachments: ["attachments"] class Collections.Posts extends Brainstem.Collection model: Models.Post url: '/api/v1/posts' Relational Models in JS -
class Views.Posts.IndexView extends Backbone.View template: JST["backbone/templates/posts/index"] initialize: -> @collection =
base.data.loadCollection "posts", include: ["project", "user", "recipients", "attachments" ...] @collection.bind 'reset', @addAll @collection.bind 'remove', @addAll render: => @$el.html @template() if @collection.loaded @addAll() else @$("#post-list").text "Just a moment..." @ Brainstem.StorageManager -
class Views.Posts.IndexView extends Backbone.View template: JST["backbone/templates/posts/index"] initialize: -> @collection =
base.data.loadCollection "posts", include: ["project", "user", "recipients", "attachments" ...] @collection.bind 'reset', @addAll @collection.bind 'remove', @addAll render: => @$el.html @template() if @collection.loaded @addAll() else @$("#post-list").text "Just a moment..." @ Brainstem.StorageManager -
class Views.Posts.IndexView extends Backbone.View template: JST["backbone/templates/posts/index"] initialize: -> @collection =
base.data.loadCollection "posts", include: ["project", "user", "recipients", "attachments" ...] @collection.bind 'reset', @addAll @collection.bind 'remove', @addAll render: => @$el.html @template() if @collection.loaded @addAll() else @$("#post-list").text "Just a moment..." @ Brainstem.StorageManager -
base.data.loadModel "users", user_id, include: ["posts"] Brainstem.StorageManager Load single models: filters
= { email: email, something: "else" } base.data.loadCollection "users", filters: filters Run filters: -
Relational Models in JS •Brainstem.js extends Backbone with relational models.
•The StorageManager ensures that any associations you ask for are available in the identity map. -
// Getting a post’s user’s name. post.get("user").get("name") // Looping over
a post’s replies. for reply in post.get("replies") console.log reply.get("text") Relational Models in JS -
This Talk • What do you want in an API?
• Why we built Brainstem • Presenters and Associations • Filters and Sorts • Brainstem.js • Decisions and Challenges • Other Libraries -
{ "posts": { "25": { "project_id": "5", "private": true, "user_id":
"45", "recipient_ids": ["1", "7", "45"], "text": "Hey guys, I'm working on RailsConf 2013 slides now.", "task_id": "7", "attachment_ids": ["1"], "reply_ids": ["9"] }, "9": { "user_id": "7", "project_id": "5", "parent_id": "25", "private": true, "text": "Looks like a great start! :thumbsup:" } } } IDs as keys -
{ "posts": { "25": { "project_id": "5", "private": true, "user_id":
"45", "recipient_ids": ["1", "7", "45"], "text": "Hey guys, I'm working on RailsConf 2013 slides now.", "task_id": "7", "attachment_ids": ["1"], "reply_ids": ["9"] }, "9": { "user_id": "7", "project_id": "5", "parent_id": "25", "private": true, "text": "Looks like a great start! :thumbsup:" } } } IDs as keys -
{ "results": [ {"key": "posts", "id": "25"}, {"key": "posts", "id":
"47"}, ... ], "posts": { "25": { "project_id": "5", "private": true, ... }, "47": { Results Array -
{ "results": [ {"key": "posts", "id": "25"}, {"key": "posts", "id":
"47"}, ... ], "posts": { "25": { "project_id": "5", "private": true, ... }, "47": { Results Array • Allows us to declare exactly which results, in what order, matched the user’s query. • Models can have associations of their own type. This leads to confusion if primary objects and associations are mixed in a single structure. -
Filters in Presenters, as scopes • Most other presenter libraries
leave filters to the user. -
Filters in Presenters, as scopes • Most other presenter libraries
leave filters to the user. • We wanted to head in the direction of requesting nested, filtered associations. -
Filters in Presenters, as scopes • Most other presenter libraries
leave filters to the user. • We wanted to head in the direction of requesting nested, filtered associations. • Easier to version filters in the presenter. -
This Talk • What do you want in an API?
• Why we built Brainstem • Presenters and Associations • Filters and Sorts • Brainstem.js • Decisions and Challenges • Other Libraries -
ActiveModel::Serializers • More mature DSL for specifying fields • Focuses
on presenting, leaves filtering, sorting, and pagination up to you. -
ActiveModel::Serializers • More mature DSL for specifying fields • Focuses
on presenting, leaves filtering, sorting, and pagination up to you. • Collaboration in our future? -
Brainstem • A presenter library for model serialization • An
API abstraction for building powerful APIs -
Brainstem • A presenter library for model serialization • An
API abstraction for building powerful APIs • An optional adaptor for Backbone -
Brainstem • A presenter library for model serialization • An
API abstraction for building powerful APIs • An optional adaptor for Backbone • Your API will be sortable, filterable, and will side- load associations, which means: -
Brainstem • A presenter library for model serialization • An
API abstraction for building powerful APIs • An optional adaptor for Backbone • Your API will be sortable, filterable, and will side- load associations, which means: • Shorter response times -
Brainstem • A presenter library for model serialization • An
API abstraction for building powerful APIs • An optional adaptor for Backbone • Your API will be sortable, filterable, and will side- load associations, which means: • Shorter response times • Fewer requests -
Brainstem • A presenter library for model serialization • An
API abstraction for building powerful APIs • An optional adaptor for Backbone • Your API will be sortable, filterable, and will side- load associations, which means: • Shorter response times • Fewer requests • Happier developers!