$30 off During Our Annual Pro Sale. View Details »

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
Tweet

More Decks by James Miller

Other Decks in Programming

Transcript

  1. Writing APIs
    With Sinatra and rack/test

    View Slide

  2. James Miller
    @bensie

    View Slide

  3. Writing APIs

    View Slide

  4. Assumptions
    Rails app that needs an API
    JSON
    Testing is important
    GitHub does it well, learn from it

    View Slide

  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...

    View Slide

  6. These are all viable options

    View Slide

  7. Except inline respond_to
    Don’t use that.

    View Slide

  8. Rails encourages MVC

    View Slide

  9. Simple Hashes

    View Slide

  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

    View Slide

  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

    View Slide

  12. Sinatra
    sinatrarb.com
    get '/' do
    'Hello world!'
    end

    View Slide

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

    View Slide

  14. Versioning / Namespacing
    sinatra/namespace

    View Slide

  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

    View Slide

  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

    View Slide

  17. Error Handling

    View Slide

  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

    View Slide

  19. halt
    def halt_with_404_not_found
    halt 404, json({ message: "Not found" })
    end
    gist.github.com/4226520

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  25. Pagination
    def paginate(relation)
    @paginated = relation.paginate(page: page, per_page: per_page)
    add_pagination_headers
    return @paginated
    end
    gist.github.com/4226520

    View Slide

  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

    View Slide

  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

    View Slide

  28. Mount it to your Rails app
    config/routes.rb
    mount Api::Endpoints, at: "/api"

    View Slide

  29. That’s the game.
    Thanks!

    View Slide