Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Brainstem, your companion for rich Rails APIs

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

April 30, 2013
Tweet

Other Decks in Programming

Transcript

  1. 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
  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
  3. You want an API for your users’ users for your

    users for yourselves you know...
  4. a fast API Is close to the database Side-loads associations

    in a single request (no N+1) Avoids object repetition
  5. 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
  6. 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
  7. { "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
  8. { "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
  9. { "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
  10. { "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
  11. { "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
  12. { "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
  13. { "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
  14. { "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
  15. { "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
  16. { "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
  17. { "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.
  18. { "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.
  19. { "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!
  20. { "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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. @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.
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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:
  37. 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.
  38. // 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
  39. 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
  40. { "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
  41. { "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
  42. { "results": [ {"key": "posts", "id": "25"}, {"key": "posts", "id":

    "47"}, ... ], "posts": { "25": { "project_id": "5", "private": true, ... }, "47": { Results Array
  43. { "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.
  44. 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.
  45. 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.
  46. 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
  47. ActiveModel::Serializers • More mature DSL for specifying fields • Focuses

    on presenting, leaves filtering, sorting, and pagination up to you.
  48. ActiveModel::Serializers • More mature DSL for specifying fields • Focuses

    on presenting, leaves filtering, sorting, and pagination up to you. • Collaboration in our future?
  49. Brainstem • A presenter library for model serialization • An

    API abstraction for building powerful APIs
  50. Brainstem • A presenter library for model serialization • An

    API abstraction for building powerful APIs • An optional adaptor for Backbone
  51. 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:
  52. 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
  53. 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
  54. 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!