Share
Embed
APIs With Sinatra
Write Rails APIs with Sinatra and rack/test.
Presented on December 6, 2012 at SDRuby.
James Miller
December 06, 2012
Transcript
29 slides
-
Assumptions Rails app that needs an API JSON Testing is
important GitHub does it well, learn from it -
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... -
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 -
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 -
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 -
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 -
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 -
halt def halt_with_404_not_found halt 404, json({ message: "Not found" })
end gist.github.com/4226520 -
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 -
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 -
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 -
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 -
Pagination def paginate(relation) @paginated = relation.paginate(page: page, per_page: per_page) add_pagination_headers
return @paginated end gist.github.com/4226520 -
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 -
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