@brandur
Post-Rails? Composable Applications with a First-class API
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
API calls happen on the backend.
class AppsController < ApplicationController
def index
@apps = @api.get_apps
end
end
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.
Deeper Composition
Manager Web
Manager API
Core API
- Manager is further composed into two tiers
- 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