Skip to content

Instantly share code, notes, and snippets.

@stevie-chambers
Created July 10, 2012 00:09
Show Gist options
  • Save stevie-chambers/3079964 to your computer and use it in GitHub Desktop.
Save stevie-chambers/3079964 to your computer and use it in GitHub Desktop.
Real basic REST API server in one script
#!/usr/bin/env ruby
#
# stevie_chambers@viewyonder.com July 2012
#
# I'm playing around with APIs, and sharing my learnings
# You can follow my trials and tribulations at http://viewyonder.com/apis
#
# This is a simple Ruby script to show how a simple API might work.
# The resource model is just a simple array of a single class - no back-end store (yet)
#
# This will take browser/curl on all four CRUD verbs and hopefully give the right HTTP responses :)
#
# Open the API root for more 'documentation'
require 'sinatra'
require 'JSON'
API_VERSION='2012-07-10'
# Our complex resource model :) We have a hash of these, key is id. That's it; that's all
class Person
attr_accessor :id, :name
def initialize(id,name)
@id = id
@name = name
end
end
steve = Person.new('1','Steve')
alan = Person.new('2','Alan')
dave = Person.new('3','Dave')
team = { '1' => steve, '2' => alan, '3' => dave }
# This bad boy is called at the end of every route to push (some of) the client's request headers for debugging.
def request_html(request)
html = Array.new
html << "<h3>Debugging</h3>"
html << "<p>Team API Version X-Api-Header - #{API_VERSION}</p>"
html << "<p>Request Method - #{request.request_method}</p>"
html << "<p>URL - #{request.url}</p>"
html << "<p>Accept - #{request.accept}</p>"
html << "<p>Body - #{request.body}</p>"
html << "<p>Query String - #{request.query_string}</p>"
html << "<p>Content Length - #{request.content_length}</p>"
html << "<p>Media Type - #{request.media_type}</p>"
html << "<p>HEADER - #{request["SOME_HEADER"]}</p>" # Use this later to authentication
html << "<p>User Agent - #{request.user_agent}</p>"
html
end
def doc_html
html = Array.new
html << "<h3>Documentation</h3>"
html << "<p>This version uses all the CRUD verbs - in order, POST, GET, PUT, DELETE.</p>"
html << "<p>The only resource in the API is /api/team and in there each member has a unique number ID.</p>"
html << "<p>This team resource is a simple hash of a simple class called Person with id and name attributes.</p>"
html << %{<li>Create a specific resource: curl -X POST -i -d '{ "id": "666", "name" : "Jeramiah" }' http://localhost:4567/api/team/666</li>}
html << %{<li>Read a specific resource: curl -X GET -i http://localhost:4567/api/team/666</li>}
html << %{<li>Update a specific resource: curl -X PUT -i -d '{ "id": "666", "name" : "Satan" }' http://localhost:4567/api/team/666</li>}
html << %{<li>Delete a specific resource: curl -X DELETE -i http://localhost:4567/api/team/666</li></ul>}
html
end
# Sinatra server root, not the api root (that's /api, not /)
get '/' do
html = Array.new
html << %{<h1>Sinatra Web Server</h1>}
html << %{<h3>Resources</h3>}
html << %{<p><a href="/api" rel="api">Team API Root End Point</a></p>}
request_html(request).each { |p| html << p }
status 200 # OK
content_type 'text/html'
body(html)
end
# The Team API root, so we give some useful info here on how to use, what's done, what's not done.
get '/api/?' do
html = Array.new
html << %{<h1>Team API</h1>}
doc_html.each { |p| html << p }
html << %{<h3>Resources</h3>}
html << %{<p><a href="/api/team" rel="team">Team Resources</a></p>}
request_html(request).each { |p| html << p }
status 200 # OK
content_type 'text/html'
body(html)
end
get '/api/team/?' do
html = Array.new
html << %{<h1>Team Resources</h1>}
doc_html.each { |p| html << p }
html << %{<h3>Resources</h3>}
team.each do |key, person|
html << %{<li><a href="/api/team/#{person.id}" rel="#{person.name}">#{person.name}</a></li>}
end
html << %{<p><a href="/api" rel="api">Team API Root End Point</a></p>}
request_html(request).each { |p| html << p }
status 200 # OK
content_type 'text/html'
body(html)
end
get '/api/team/:id' do
person = team[params[:id]]
if person.nil? then
html = Array.new
html << %{<h1>Team Resource - Error</h1>}
html << %{<p>NOT FOUND: Unable to find the team member with ID: #{params[:id]}</p>}
html << %{<p><a href="/api/team" rel="team">Team Resources</a></p>}
request_html(request).each { |p| html << p }
status 404 # Not Found
content_type 'text/html'
body(html)
else
html = Array.new
doc_html.each { |p| html << p }
html << %{<h1>Team Resource - #{person.name}</h1>}
html << %{<p>ID - #{person.id}</p>}
html << %{<p>Name - #{person.name}</p>}
html << %{<p><a href="/api/team" rel="team">Team Resources</a></p>}
request_html(request).each { |p| html << p }
status 200 # OK
content_type 'text/html'
body(html)
end
end
delete '/api/team/:id' do
person = team[params[:id]]
if person.nil? then
html = Array.new
html << %{<h1>Team Resource</h1>}
html << %{<p>NOT FOUND: Unable to find the team member with ID: #{params[:id]}</p>}
html << %{<p><a href="/api/team" rel="team">Team Resources</a></p>}
request_html(request).each { |p| html << p }
status 404 # Not Found
content_type 'text/html'
body(html)
else
name = team[params[:id]].name
team.delete(params[:id])
html = Array.new
html << %{<h1>Team Resource</h1>}
html << %{<p>ACCEPTED: Deleted team member #{name} with ID #{params[:id]}</p>}#
html << %{<p><a href="/api/team" rel="team">Team Resources</a></p>}
request_html(request).each { |p| html << p }
status 202 # Accepted
content_type 'text/html'
body(html)
end
end
post '/api/team/:id' do
data = JSON.parse(request.body.string)
if data.nil? or !data.has_key?('id') or !data.has_key?('name') then
html = Array.new
html << %{<h1>Team Resources</h1>}
html << %{<p>BAD REQUEST: Unable to create the team member, insufficient data (missing id or name)</p>}
html << %{<p><a href="/api/team" rel="team">Team Resources</a></p>}
request_html(request).each { |p| html << p }
status 400 # Bad Request
content_type 'text/html'
body(html)
else
html = Array.new
html << %{<h1>Team Resourcer</h1>}
if team[data['id']] then
html << %{<p>FORBIDDEN: Unable to create the team member, id #{data['id']} already exists</p>}
html << %{<p><a href="/api/team/#{data['id']}">Go to see existing team member</a></p>}
status 403 # Forbidden
else
person = Person.new(data['id'],data['name'])
team[data['id']] = person
html << %{<p>CREATED: id: #{person.id} name: #{person.name}</p>}
status 201 # Created
end
html << %{<p><a href="/api/team" rel="team">Team Resources</a></p>}
request_html(request).each { |p| html << p }
content_type 'text/html'
body(html)
end
end
put '/api/team/:id' do
data = JSON.parse(request.body.string)
if data.nil? or !data.has_key?('id') or !data.has_key?('name') then
html = Array.new
html << %{<h1>Team Resources</h1>}
html << %{<p>BAD REQUEST: Unable to update the team member, insufficient request data (missing id or name)</p>}
html << %{<p><a href="/api/team" rel="team">Team Resources</a></p>}
request_html(request).each { |p| html << p }
status 400 # Bad Request
content_type 'text/html'
body(html)
else
html = Array.new
html << %{<h1>Team Resourcer</h1>}
if !team[data['id']] then
html << %{<p>NOT FOUND: Unable to update the team member, id #{data['id']} doesn't exist</p>}
status 404 # Not Found
else
person = Person.new(data['id'],data['name'])
team[data['id']] = person
html << %{<p>ACCEPTED: Updated team member id #{person.id} to #{person.name}</p>}
status 202 # Accepted
end
html << %{<p><a href="/api/team" rel="team">Team Resources</a></p>}
request_html(request).each { |p| html << p }
content_type 'text/html'
body(html)
end
end
after do
response['X-Api-Header'] = API_VERSION
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment