View
4.929
Download
4
Category
Preview:
Citation preview
Using Sinatra to Build REST APIs in Ruby
James Higginbotham API Architect @launchany
Introduc?on
WHAT IS SINATRA?
Sinatra is a DSL for quickly crea<ng web applica<ons in
Ruby
# hi.rb require 'rubygems’ require 'sinatra' get '/' do 'Hello world!’ end
$ gem install sinatra $ ruby hi.rb == Sinatra has taken the stage ... >> Listening on 0.0.0.0:4567 $ curl http://0.0.0.0:4567 Hello World
HOW DOES SINATRA WORK?
Rou?ng: Verb + PaCern + Block post ’/' do .. block .. end
Rou?ng: Named Params get '/:id' do model = MyModel.find( params[:id] ) ... end
Rou?ng: Splat Support get '/say/*/to/*' do # matches /say/hello/to/world params['splat'] # => ["hello", "world"] ... end get '/download/*.*' do # matches /download/path/to/file.xml params['splat'] # => ["path/to/file", "xml"] ... end
Rou?ng: Regex Support get /\A\/hello\/([\w]+)\z/ do "Hello, #{params['captures'].first}!” ... end
Rou?ng: Op?onal Parameters get '/posts.?:format?' do # matches "GET /posts" and # any extension "GET /posts.rss", "GET /posts.xml" etc. end
Rou?ng: URL Query Parameters get '/posts' do # matches "GET /posts?title=foo&author=bar" title = params['title'] author = params['author'] # uses title and author variables; # query is optional to the /posts route End
Rou?ng: Condi?onal Matching get '/', :host_name => /^admin\./ do "Admin Area, Access denied!" end get '/', :provides => 'html' do haml :index end get '/', :provides => ['rss', 'atom', 'xml'] do builder :feed end
Rou?ng: Custom Condi?ons set(:probability) { |value| condition { rand <= value } } get '/win_a_car', :probability => 0.1 do "You won!" end get '/win_a_car' do "Sorry, you lost." End
Returning Results # 1. String containing the body and default code of 200 get '/' do 'Hello world!’ end # 2. Response code + body get '/' do [200, 'Hello world!’] end # 3. Response code + headers + body get '/' do [200, {'Content-Type' => 'text/plain'}, 'Hello world!’] end
BUILDING ON RACK
Hello World with Rack # hello_world.rb require 'rack' require 'rack/server’ class HelloWorldApp def self.call(env) [200, {}, 'Hello World’] end end Rack::Server.start :app => HelloWorldApp
Rack env # hello_world.rb require 'rack' require 'rack/server’ class HelloWorldApp def self.call(env) [200, {}, "Hello World. You said: #{env['QUERY_STRING']}"] end end Rack::Server.start :app => HelloWorldApp
Typical env { "SERVER_SOFTWARE"=>"thin 1.4.1 codename Chromeo", "SERVER_NAME"=>"localhost", "rack.input"=>#<StringIO:0x007fa1bce039f8>, "rack.version"=>[1, 0], "rack.errors"=>#<IO:<STDERR>>, "rack.multithread"=>false, "rack.multiprocess"=>false, "rack.run_once"=>false, "REQUEST_METHOD"=>"GET", "REQUEST_PATH"=>"/favicon.ico", "PATH_INFO"=>"/favicon.ico", "REQUEST_URI"=>"/favicon.ico", "HTTP_VERSION"=>"HTTP/1.1", "HTTP_HOST"=>"localhost:8080", "HTTP_CONNECTION"=>"keep-alive", "HTTP_ACCEPT"=>"*/*”, ...
Typical env (con’t) ... "HTTP_USER_AGENT"=> "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.47 Safari/536.11", "HTTP_ACCEPT_ENCODING"=>"gzip,deflate,sdch", "HTTP_ACCEPT_LANGUAGE"=>"en-US,en;q=0.8", "HTTP_ACCEPT_CHARSET"=>"ISO-8859-1,utf-8;q=0.7,*;q=0.3", "HTTP_COOKIE"=> "_gauges_unique_year=1; _gauges_unique_month=1", "GATEWAY_INTERFACE"=>"CGI/1.2", "SERVER_PORT"=>"8080", "QUERY_STRING"=>"", "SERVER_PROTOCOL"=>"HTTP/1.1", "rack.url_scheme"=>"http", "SCRIPT_NAME"=>"", "REMOTE_ADDR"=>"127.0.0.1", ... }
The Rack::Request Wrapper class HelloWorldApp def self.call(env) request = Rack::Request.new(env) request.params # contains the union of GET and POST params request.xhr? # requested with AJAX require.body # the incoming request IO stream if request.params['message'] [200, {}, request.params['message']] else [200, {}, 'Say something to me!'] end end end
Rack Middleware
u Rack allows for chaining mul?ple call() methods
u We can do anything we want within each call() u This includes separa?ng behavior into reusable classes (e.g. across Sinatra and Rails)
u SRP (Single Responsibility Principle) – Each class has a single responsibility – Our app is composed of mul?ple classes that each do one thing well
Rack::Builder for Middleware # this returns an app that responds to call cascading down # the list of middlewares. app = Rack::Builder.new do use Rack::Etag # Add an ETag use Rack::ConditionalGet # Support Caching use Rack::Deflator # GZip run HelloWorldApp # Say Hello end Rack::Server.start :app => app # Resulting call tree: # Rack::Etag # Rack::ConditionalGet # Rack::Deflator # HelloWorldApp
Using the Rackup Command
u Combines all of these concepts into a config u Will start a web process with your Rack app u Central loca?on for requires, bootstrapping u Enables middleware to be configured as well u Default filename is config.ru u Used to bootstrap Rails
Using Rackup # config.ru # HelloWorldApp defintion # EnsureJsonResponse defintion # Timer definition use Timer use EnsureJsonResponse run HelloWorldApp
$ rackup –p 4567
Using Mul?ple Sinatra Apps
u Rackup allows for moun?ng mul?ple Sinatra Apps
u This allows for more modular APIs u Recommend one Sinatra app per top-‐level resource
Moun?ng Mul?ple Sinatra Apps # config.ru require 'sinatra' require 'app/auth_api' require 'app/users_api' require 'app/organizations_api' map "/auth" do run AuthApi end map "/users" do run UsersApi end map "/organizations" do run OrganizationsApi end
Important: Require != Automa?c
u Must manage your own requires u No free ride (like with Rails) u This means order of requires is important!
WHAT IS A REST API?
Mul?ple API Design Choices
u RPC-‐based – Uses HTTP for transport only – Endpoints are not unique, only the payload – No HTTP caching available – e.g. POST /getUserDetails, POST /createUser
u Resource-‐based – Unique URLs for resources and collec?ons – HTTP caching available – e.g. GET /users/{userId} and GET /users
Hypermedia REST
u An architectural style, with constraints u A set of constraints, usually on top of HTTP u Not a standard; builds on the standard of HTTP
u Mul?ple content types (e.g. JSON, XML, CSV) u The response is a representa?on of the resource state (data) plus server-‐side state in the form of ac<ons/transi<ons (links)
BUILDING AN API USING SINATRA
Resource Lifecycle using Sinatra get '/users' do .. list a resource collection (and search) .. end get '/users/:id' do .. resource instance details .. end post '/users' do .. create new resource .. end put '/users/:id' do .. replace resource .. End delete ’/users/:id' do .. annihilate resource .. end
List Resources Example get '/users' do # 1. capture any search filters using params[] email_filter = params[:email] # 2. build query and fetch results from database if email_filter users = User.where( email: email_filter ).all else users = User.all # 3. marshal results to proper content type (e.g. JSON) [200, users.to_json] end
List Resources Example get '/users' do # 1. capture any search filters using params[] email_filter = params[:email] # 2. build query and fetch results from database if email_filter users = User.where( email: email_filter ).all else users = User.all # 3. marshal results to proper content type (e.g. JSON) [200, users.to_json] # Q: Which ORM should we use with Sinatra? # Q: Can we customize the results format easily? end
USEFUL GEMS
Selec?ng an ORM
u Ac?veRecord u DataMapper u Sequel (my favorite) – Flexible as it supports Datasets and Models
Sequel Datasets Example require 'sequel' DB = Sequel.sqlite # memory database DB.create_table :items do primary_key :id String :name Float :price end items = DB[:items] # Create a dataset items.insert(:name => 'abc', :price => rand * 100) items.insert(:name => 'def', :price => rand * 100) items.insert(:name => 'ghi', :price => rand * 100) puts "Item count: #{items.count}" puts "The average price is: #{items.avg(:price)}”
Sequel Model Example require 'sequel' DB = Sequel.sqlite # memory database class Post < Sequel::Model end post = Post[123] post = Post.new post.title = 'hello world' post.save
Select a Marshaling Library
u Ac?veModel::Serializers (AMS) – Works with Kaminari and WillPaginate – Supported by Rails core team – One-‐way JSON genera?on only
u Roar+Representable (my favorite) – Works with and without Rails – Bi-‐direc?onal marshaling – Supports JSON, XML, YAML, hash
Representable module SongRepresenter include Representable::JSON property :title property :track collection :composers end class Song < OpenStruct end song = Song.new(title: "Fallout", track: 1) song.extend(SongRepresenter).to_json > {"title":"Fallout","track":1} song = Song.new.extend(SongRepresenter).from_json(%{ {"title":"Roxanne"} }) > #<Song title="Roxanne">
Roar + Representable module SongRepresenter include Roar::JSON include Roar::Hypermedia property :title property :track collection :composers link :self do "/songs/#{title}" end end song = Song.new(title: "Fallout", track: 1) song.extend(SongRepresenter).to_json > {"title":"Fallout","track":1,"links": [{"rel":"self","href":"/songs/Fallout"}]}"
Tools for Tes?ng Your API
u Unit – RSpec – Models, helpers
u Integra?on – RSpec – Make HTTP calls to a running Sinatra process – Controller-‐focused
u Acceptance/BDD – RSpec, Cucumber – Make HTTP calls to a running Sinatra process – Use-‐case/story focused
MATURING YOUR SINATRA APPS
Addi?onal Gems
u faraday – HTTP client with middleware for tes?ng and 3rd party API integra?on
u xml-‐simple – Easy XML parsing and genera?on u faker – Generates fake names, addresses, etc. u uuidtools – uuid generator when incremen?ng integers aren’t good enough
u bcrypt – Ruby binding for OpenBSD hashing algorithm, to secure data at rest
Addi?onal Gems (part 2)
u rack-‐conneg – Content nego?a?on support
get '/hello' do response = { :message => 'Hello, World!' } respond_to do |wants| wants.json { response.to_json } wants.xml { response.to_xml } wants.other { content_type 'text/plain' error 406, "Not Acceptable" } end end curl -H "Accept: application/json" http://localhost:4567/hello
Addi?onal Gems (part 3)
u hirb – Console formaing of data from CLI, Rake tasks
irb>> Tag.last +-----+-------------------------+-------------+ | id | created_at | description | +-----+-------------------------+-------------+ | 907 | 2009-03-06 21:10:41 UTC | blah | +-----+-------------------------+-------------+ 1 row in set
Reloading with Shotgun Gem
u No automa?c reload of classes with Sinatra u Instead, use the shotgun gem:
u Note: Only works with Ruby MRI where fork() is available (POSIX)
$ gem install shotgun $ shotgun config.ru
Puma + JRuby
u Ruby MRI is geing beCer u JVM is faster (2-‐5x), very mature (since 1997) u High performance garbage collectors, na?ve threading, JMX management extensions
u JDBC libraries very mature and performant for SQL-‐based access
u Puma is recommended over unicorn for JRuby
From Sinatra to Padrino
u Padrino provides Rails-‐like environment for Sinatra
u Build in Sinatra, move to Padrino when needed
u Generators, pluggable modules, admin generator
Resources
u Sinatra Docs: hCp://www.sinatrarb.com/intro.html
u Introduc?on to Rack: hCp://hawkins.io/2012/07/rack_from_the_beginning/
u Sequel Gem: hCps://github.com/jeremyevans/sequel
u Roar/Representable: hCps://github.com/apotonick/roar hCps://github.com/apotonick/representable
Thanks Ya’ll
James Higginbotham james@launchany.com hCp://launchany.com
@launchany
Design Beau?ful APIs: hCp://TheApiDesignBook.com
QUESTIONS
Recommended