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.

02734bb0afe24b614f37a54eada76538?s=128

Andrew Cantino

April 30, 2013
Tweet

Transcript

  1. Brainstem Your companion for rich Rails APIs Andrew Cantino @tectonic

  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. 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
  4. You want an API

  5. You want an API for your users you know...

  6. You want an API for your users’ users for your

    users you know...
  7. You want an API for your users’ users for your

    users for yourselves you know...
  8. You want a consistent API

  9. You want a consistent API dates

  10. You want a consistent API dates IDs

  11. You want a consistent API dates IDs routes

  12. You want a consistent API dates IDs routes JSON

  13. You want a versioned API

  14. You want a fast API

  15. a fast API

  16. a fast API

  17. a fast API Is close to the database

  18. a fast API Is close to the database Side-loads associations

    in a single request (no N+1)
  19. a fast API Is close to the database Side-loads associations

    in a single request (no N+1) Avoids object repetition
  20. 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
  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. Displaying a post

  23. Displaying a post with associated data...

  24. None
  25. Project Title Post Recipients The Post Attachment Linked Task Reply

    poster Reply
  26. { "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
  27. { "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
  28. { "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
  29. { "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
  30. { "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
  31. { "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
  32. { "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
  33. { "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
  34. { "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
  35. { "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
  36. { "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.
  37. { "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.
  38. { "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!
  39. { "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
  40. 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
  41. Presenter? AR { }

  42. 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
  43. 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
  44. 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
  45. 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
  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. 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
  48. 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
  49. 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
  50. 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
  51. Brainstem + Backbone.js

  52. Brainstem.js

  53. @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.
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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:
  59. Relational Models in JS

  60. Relational Models in JS •Brainstem.js extends Backbone with relational models.

  61. 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.
  62. // 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
  63. 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
  64. { "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
  65. { "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
  66. { "results": [ {"key": "posts", "id": "25"}, {"key": "posts", "id":

    "47"}, ... ], "posts": { "25": { "project_id": "5", "private": true, ... }, "47": { Results Array
  67. { "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.
  68. Filters in Presenters, as scopes

  69. Filters in Presenters, as scopes • Most other presenter libraries

    leave filters to the user.
  70. 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.
  71. 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.
  72. 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
  73. ActiveModel::Serializers

  74. ActiveModel::Serializers • More mature DSL for specifying fields

  75. ActiveModel::Serializers • More mature DSL for specifying fields • Focuses

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

    on presenting, leaves filtering, sorting, and pagination up to you. • Collaboration in our future?
  77. Brainstem

  78. Brainstem • A presenter library for model serialization

  79. Brainstem • A presenter library for model serialization • An

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

    API abstraction for building powerful APIs • An optional adaptor for Backbone
  81. 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:
  82. 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
  83. 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
  84. 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!
  85. https://github.com/mavenlink/brainstem https://github.com/mavenlink/brainstem-js

  86. Any Questions? Andrew Cantino @tectonic andrewcantino.com https://github.com/mavenlink/brainstem https://github.com/mavenlink/brainstem-js