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

APIs With Sinatra

James Miller
December 06, 2012

APIs With Sinatra

Write Rails APIs with Sinatra and rack/test.
Presented on December 6, 2012 at SDRuby.

James Miller

December 06, 2012

More Decks by James Miller

Other Decks in Programming


  1. Assumptions Rails app that needs an API JSON Testing is

    important GitHub does it well, learn from it
  2. Existing Options respond_to / respond_with inline in controller ActiveModel::Serializers with

    rails_api Separate Rails controller(s), Jbuilder or RABL Grape - github.com/intridea/grape Lots of other options...
  3. Simple Hashes class User < ActiveRecord::Base def api_base_hash { id:

    id, login: login, name: name, email: email, created_at: created_at.utc.iso8601 } end def api_full_hash api_base_hash.merge({ updated_at: updated_at.utc.iso8601 }) end def api_authenticated_hash api_full_hash.merge({ plan: plan, cc_last4: cc_last4 }) end end gist.github.com/4226520
  4. Simple Hashes gist.github.com/4226520 class Event < ActiveRecord::Base belongs_to :user def

    api_base_hash { id: id, name: name } end def api_full_hash api_base_hash.merge({ user: user.api_base_hash }) end end
  5. Versioning / Namespacing module Api class Endpoints < Base get

    "/" do json({ message: "Welcome to the API" }) end get "/v1" do json({ message: "This is version 1 of the API" }) end namespace "/v1" do get "/me" do authenticate! json current_user.api_authenticated_hash end end end end gist.github.com/4226520
  6. Test it! describe Api::Endpoints do let(:browser) { Rack::Test::Session.new(...) } describe

    "base" do it "responds with JSON at the root" do browser.get("/") should_200({message: "Welcome to the API"}) should_be_json end end end gist.github.com/4226520
  7. 404 gist.github.com/4226520 # Any unmatched request within the /api/ namespace

    should render 404 as JSON # Stop the request here so that JSON gets returned instead of having it # run through the whole Rails stack and spit HTML. get "/*" do halt_with_404_not_found end post "/*" do halt_with_404_not_found end put "/*" do halt_with_404_not_found end patch "/*" do halt_with_404_not_found end delete "/*" do halt_with_404_not_found end
  8. Test it! it "responds with 404 JSON at misc not

    found paths" do browser.get("/a") should_404 should_be_json browser.get("/a-b") should_404 should_be_json browser.get("/a/b/c") should_404 should_be_json end gist.github.com/4226520
  9. halt def halt_with_400_bad_request(message = nil) message ||= "Bad request" halt

    400, json({ message: message }) end def halt_with_401_authorization_required(message = nil, realm = "App Name") message ||= "Authorization required" headers 'WWW-Authenticate' => %(Basic realm="#{realm}") halt 401, json({ message: message }) end def halt_with_403_forbidden_error(message = nil) message ||= "Forbidden" halt 403, json({ message: message }) end def halt_with_500_internal_server_error halt 500, json({ message: env['sinatra.error'].message }) end gist.github.com/4226520
  10. error error ActiveRecord::RecordNotFound do halt_with_404_not_found end error ActiveRecord::RecordInvalid do halt_with_422_unprocessible_entity

    end error ActiveRecord::UnknownAttributeError do halt_with_422_unprocessible_entity end error ActiveRecord::DeleteRestrictionError do halt_with_400_bad_request end error MultiJson::DecodeError do halt_with_400_bad_request("Problems parsing JSON") end error do if ::Exceptional::Config.should_send_to_api? ::Exceptional::Remote.error(::Exceptional::ExceptionData.new(env['sinatra.error'])) end halt_with_500_internal_server_error end gist.github.com/4226520
  11. Test it! describe "events" do before do @user = User.create!

    User.prototype @event = @user.events.create! Event.prototype end it "should fetch a collection of events" do browser.get("/v1/users/#{@user.login}/events") should_200([@event.api_base_hash]) end end gist.github.com/4226520
  12. App Endpoints post "/events" do event = current_user.events.create!(parsed_request_body) status 201

    json event.api_authenticated_hash end put "/events/:event_id" do event = current_user.events.find(params[:event_id]) event.update_attributes!(parsed_request_body) json event.api_authenticated_hash end delete "/events/:event_id" do current_user.events.find(params[:event_id]).destroy status 204 end gist.github.com/4226520
  13. Test it! describe "events" do before do @user = User.create!

    User.prototype authorize(@user) end it "should create a new event" do browser.post("/v1/me/events", json({name: "Foo", date: "2012-06-09"})) should_201 @user.events.count.should == 1 lrb = decode_json(browser.last_response.body) lrb.should == @user.events.first.api_authenticated_hash end it "should fail to create a event if missing required fields" do browser.post("/v1/me/events", json({})) should_422 end end gist.github.com/4226520