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. Writing APIs With Sinatra and rack/test

  2. James Miller @bensie

  3. Writing APIs

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

    important GitHub does it well, learn from it
  5. 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...
  6. These are all viable options

  7. Except inline respond_to Don’t use that.

  8. Rails encourages MVC

  9. Simple Hashes

  10. 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
  11. 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
  12. Sinatra sinatrarb.com get '/' do 'Hello world!' end

  13. rack/test github.com/brynary/rack-test

  14. Versioning / Namespacing sinatra/namespace

  15. 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
  16. 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
  17. Error Handling

  18. 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
  19. halt def halt_with_404_not_found halt 404, json({ message: "Not found" })

    end gist.github.com/4226520
  20. 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
  21. 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
  22. 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
  23. App Endpoints get "/users/:user_id/events" do user = User.find(params[:user_id]) json paginate(user.events).map(&:api_base_hash)

    end gist.github.com/4226520
  24. 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
  25. Pagination def paginate(relation) @paginated = relation.paginate(page: page, per_page: per_page) add_pagination_headers

    return @paginated end gist.github.com/4226520
  26. 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
  27. 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
  28. Mount it to your Rails app config/routes.rb mount Api::Endpoints, at:

  29. That’s the game. Thanks!