@brandur

  • Explain Heroku

Post-Rails? Composable Applications with a First-class API

A Study of
Composition

Heroku's API Team

  • If you've used the CLI, you've used the API
    heroku list
    heroku create
    GET https://api.heroku.com/apps
    POST https://api.heroku.com/apps
  • Try it!
    # you can your API key from ~/.netrc
    curl --user ":$API_KEY" -i https://api.heroku.com/apps

We were also in charge of this.

  • Our "monorail"
  • Wrap our heads around the entire app

Meet Core

  • A Rails app
  • Then, a BIG RAILS APP
  • Severe impact on development velocity and effort

We broke things out.

  • Identify a logical layer
  • Talk about advantages of this technique later
  • Process management
  • Billing
  • Addons
  • Domains

BUTC

Break up the core

  • Many companies run into this problem, one is our parent Salesforce

Still too big

  • The goal is something small and agile
  • Rails taught us to design an API like this:
    responds_to do |format|
      format
    .json { render :json => @apps }
      format
    .html
    end
  • But we're an API, why should the web component get special treatment?

Introducing Dashboard

Dashboard Implementation

  • Rails
  • Heroku.rb: the same backend that handles API calls for the CLI
    # GET /apps
    def get_apps
      request
    (
       
    :expects  => 200,
       
    :method   => :get,
       
    :path     => "/apps"
     
    )
    end

API calls happen on the backend.

class AppsController < ApplicationController
 
def index
   
@apps = @api.get_apps
 
end
end

Where's the fat client?

  • We enjoy the Ruby + Rails experience
  • Puts all the elements in place for a single page app
    • Graceful degredation
  • Backbone still recommends bootstrapped models
    <script>
     
    var Apps = new Backbone.Collection;
     
    Apps.reset(<%= @apps.to_json %>);
    </script>

Metrics

  • Dashboard
    • +4500 LOCs (3200 Ruby + 1300 templates)
  • Core
    • 66k LOCs (61k Ruby + 5k templates)
    • Down to 55k LOCs (55k Ruby)
    • -11k LOCs (~15%)

Not the final win,
but one we'll take gladly.

=

+

Our API is now an API.

  • The responds_to block is no longer needed.

Heroku Manager

Deeper Composition

Manager Web
Manager API
Core API
  • Manager is further composed into two tiers
    • API
    • Web
  • Manager's API layer injects additional business logic built on underlying primitives: teams & organizations

A good API is a reusable API

Dashboard
CLI
Manager
Core API
  • Core's API separately consumed by Dashboard, Manager, and the CLI

App
Composition

Composability

Web
Our web users

API
CLI, Dashboard, Manager API + developers

Internal Services

Service Oriented Architecture (SOA)

  • Let's use a buzzword!
  • Users also benefit from these strong contracts
  • Loosely coupled components
  • Encourages strong contracts (they're a necessity)
  • Independent scaling of each service

Not a new idea.

Service-Oriented Architecture: Concepts, Technology, and Design

*2005

  • Eight principles from industry
  • Published by Thomas Erl

First-class APIs

  • All I mean by first-class is that it's not added as an afterthought or default, like in the responds_to block.
  • Why would you ever want to promote this? API is a critical factor the increasingly important mobile component
  • API design considered at least as much as web
  • Framework conducive to API development
  • Let's talk options

Rails as frontend

  • Great at interface
    • Template options
    • Asset pipeline
  • Maintenance of state through cookies + sessions
  • Security considerations: XSS/CSRF/etc.

Rails as API

  • We're not post-Rails, but we did move it up a layer
  • Helpers/views become less useful
  • ActiveRecord pretty good, but it's now decoupled
  • ActiveRecord::Serialization#to_json is a bad idea when strong contracts are important
  • RESTful APIs are about verbs and nouns
    • A routing DSL is an unneeded layer of abstraction

rails-api as API

  • Like everything in the Ruby world, there's a project for that
gem install rails-api
rails-api new facts_api
  • Plugin by Yehuda Katz, José Valim, Carlos Antonio da Silva, and Santiago Pastorino
  • Originally in Rails core, but reverted
  • Selection of just the middleware needed for an API
  • Drops views, helpers, and assets
  • Familiarity

Sinatra as API

  • José Valim's InlineRoutes -- https://gist.github.com/3717973
  • For full stack control
get "/facts/:id" do |id|
  fact
= Fact.first(id: id.to_i)
 
[200, encode_json(fact)]
end

post
"/facts" do
  fact
= Fact.new(fact_params)
  fact
.save
 
[201, encode_json(fact)]
end
  • The API's verbs and nouns are explicit
  • Beyond a trivial app, forces consideration of project structure
  • Use the exact Rack middleware stack you need

Grape as API

class Facts::API < Grape::API
  version
'v0', using: :header

  resources
:facts do
   
get ":id" do
      fact
= Fact.first(id: params[:id].to_i)
      encode_json
(fact)
   
end

    post
do
      fact
= Fact.create(fact_params)
      encode_json
(fact)
   
end
 
end
end
  • Patterns for versioning, parameter validation and coercion, and endpoint descriptions

Organization

  • In many cases where a service was broken off, it was done by someone who cared about that portion, and that team member would go with it
  • Smaller and more specialized teams
    • Before: three backend people, a designer, and a handful of frontend engineers

      ⚥ ⚥ ⚥ ⚥ 

    • After: API is three backend people; Dashboard is a designer and a frontend engineer

      ⚥ ⚥ ⚥ ↔ ⚥ 

Happiness

  • Frontend engineers not burdened by technical complexity of backend
  • Internal self-service means fewer people bothering each other
  • Designers and frontend people get to work on a thin web app
  • Same goes for the backend people
  • Internal self-service

And the best part?

Backend people
don't

have to think
about CSS floats

Flexibility

  • Use an agnostic protocol (stay HTTP)
  • Then use the right tool for the right job
    • Core API: Rails
    • Dashboard: Rails
    • Manager API: Scala
    • Manager Web: Sinatra + Backbone

Lessons Learn(t/ing)

Stubs

  • Do I have to set up ten different apps to get an app bootstrapped?
  • More pieces in the system make development and testing harder
  • You've just composed your apps to streamline your work; setup should also be streamlined

We started with this.

  • This is not exactly what it looked like, but it's the right idea.

But we don't recommend it.

class AppsController < ApplicationController
 
def index
   
@apps = if production?
     
@api.get_apps
   
else
     
[]
   
end
 
end
end

Artifice

  • Routes network calls to a Rack app
  • Yehuda Katz's artifice that does Net::HTTP; we're using excon-artifice (also allows multiple stubs)
  • Easy stubbing for testing and development
stub(Config).billing_api { "https://billing-api.localhost" }
stub
(Config).process_api { "https://process-api.localhost" }

# stub with fully functional Rack apps
Artifice::Excon.activate_for(Config.billing_api,
 
BillingAPIStub.new)
Artifice::Excon.activate_for(Config.process_api,
 
ProcessAPIStub.new)

And you get Rack apps!

web:         thin start -R config.ru -p $PORT
billing_api
: thin start -R stubs/billing_api.ru -p $PORT
process_api
: thin start -R stubs/process_api.ru -p $PORT

These apps are platform deployable.

Teams should own their own API stubs.

  • This is one we're still thinking about
  • API change --> you could run against all known internal test suites
  • We're also thinking about how smart the stubs should be? A very dumb version of the service they're stubbing or the most basic stub possible.

Platform

  • Takeaway: don't let ops team control design decisions because platform is hard
  • We deploy a lot of stuff to Heroku, but creating a new kernel app isn't too bad either
  • Creating a new app should be trivial
  • Pain to deploy a new app formation should be low
  • Easy reconfiguration
    • Web can point to a production API or a deployed stub

I know somebody that does all this.

@brandur

brandur@mutelight.org