Key Takeaways
- The REST (Representational State Transfer) architecture is a convenient and mature mechanism for shuttling data between clients and servers, which can be effectively implemented using Sinatra, a Domain Specific Language (DSL) for creating web applications in Ruby.
- Sinatra’s routing language includes the same verbs used by HTTP, including GET, POST, PUT and DELETE, making it well-suited to REST API transactions. This is particularly useful for applications with highly dynamic data, such as online games, where it is more efficient for clients to render their own view.
- The article provides a walkthrough of implementing a server-side REST API with Sinatra using a simple card game as an example. This involves creating a deck of cards as an array of strings, and three card-playing functions for the server: join_game, deal, and get_cards.
- The Sinatra REST API is implemented using GET and POST verbs, with each of the three main functions of the card game provided with its own URL. JSON-based values are returned to the players.
- A client-side REST API is also implemented to mimic the game-playing methods on the server, using the rest-client Ruby library. This involves creating minimally functional methods that join the game, deal cards, and get a copy of the cards.
The REpresentational State Transfer (REST) architecture provides a very convenient mechanism to shuttle data between clients and servers. Web services and protocols, like HTTP, have been using the REST architecture for many years, so it is a well-tested and mature concept.
Sinatra can be used to effectively implement the REST architecture. Sinatra’s DSL (Domain Specific Language) seems custom-tuned to the spirit of REST. In fact, Sinatra’s routing language includes the same verbs used by HTTP, including GET
, POST
, PUT
and DELETE
. Because these REST API transactions look and behave very much like HTTP, network devices will treat your REST transactions in virtually the same way.
In this article, we’ll look at a server-side implementation of REST using Sinatra. We’ll also look at a simple client-side REST application to validate the Sinatra server implementation. We’ll demonstrate using a simple card game, where a single deck of cards will be held by the server and dealt out to four clients. In the end, we’ll see how simple it is to create applications that run across clients and servers, using REST to help shuttle data back and forth.
A quick question before we get started: Why not use Ruby on Rails to move data around? Well, you certainly could use Rails (or any other web framework), but consider that Rails not only retrieves data, but it renders the view as well. This rendering process puts an extra load on the server, especially when the data is highly dynamic and cannot be cached. When it comes to online games, for example, data continuously changes. It’s therefore better to let the clients render their own view rather than forcing the server to re-render the view each time a player makes a move.
Setting Up Sinatra
Before installing Sinatra, it helps to understand how it works and what is required to make it work.
As mentioned above, Sinatra is a DSL. It essentially wraps another program, called “Rack,” and makes it more palatable to Ruby programmers. For example, Sinatra allows you to write the following “route,” which captures a client HTTP GET
request for a page located at http://www.example.com/getmypage.html:
get '/getmypage.html' do
'Hello World!'
end
With the above code running on the server under Sinatra, a browser pointed toward the URL would retrieve a simple “Hello World” response. (Note that, for simplicity, I didn’t wrap the response in HTML tags.)
Rack provides a standard interface between Ruby web frameworks (like Sinatra and Rails) and the actual web server (like WEBrick or Thin).
In this example, we’ll use Sinatra + Rack + Thin as the basis for our server.
To set up this environment, make sure you have installed the gems for Sinatra, Rack and Thin. Generally, when you install Sinatra, it will install Rack. Thin, will not automatically install, so you’ll need to do that manually.
$ gem install sinatra
$ gem install thin
Once Sinatra has been installed, you simply need to require
it in your source files. It will automatically use the Thin web server.
You can run this very simple file, myapp.rb
, as shown below, and test it locally. (This test file was lifted directly from the GitHub Sinatra page.)
# myapp.rb
require 'sinatra'
get '/' do
'Hello World'
end
Run the file with this command line:
ruby myapp.rb
You can then view the results on your local web browser at http://localhost:4567
Note that the TCP socket is automatically assigned to port 4567. You can change it to the standard port 80 when you deploy the application.
Also note that, as a DSL, Sinatra allows you to implement PUT
, POST
and DELETE
as well as the GET
verb. For example, we could process a client’s POST
request as follows:
post '/join' do
# process the POST command here ...
end
The above code could be placed in the myapp.rb
file, just below the previous get
route. The Sinatra routine simply scans down the file, from beginning to end, looking for verbs and processing them accordingly.
See the Sinatra website for excellent documentation as well as tips for getting started with this lightweight and efficient web DSL.
The Card Game
We’ll implement the beginnings of an entertaining card game called Uno, played with all cards of the deck, including two jokers. The main purpose of this game is to illustrate the Sinatra REST API in action, so we’ll not fully develop the game logic.
We create a deck of cards as a single array of strings, where each string unambiguously describes a card. For example, “4-diamond” represents the four-of-diamonds card.
We’ll create three card-playing functions for the server:
join_game
— allow a player to join the gamedeal
— shuffle and deal cards to each playerget_cards
— present dealt cards to a player
We’ll limit the game to four players, and the server will only run one game at a time.
To implement all this logic, we’ll develop two files:
uno-server.rb
— Contains the game-player logic as well as the Sinatra REST API server codeuno-client.rb
— Contains the client player logic
The core of the card game runs within the UnoServer
class, as shown below. Note that this class is completely independent of the server, though the server code will be placed into the same file.
# Ruby Uno Server
require 'sinatra'
require 'json'
class UnoServer
attr_reader :deck, :pool, :hands, :number_of_hands
MAX_HANDS = 4
def initialize
@hands = Array.new
@number_of_hands = 0
@pool = Array.new
@deck = %w(2-diamond 3-diamond 4-diamond 5-diamond 6-diamond 7-diamond 8-diamond 9-diamond 10-diamond)
@deck.concat %w(2-heart 3-heart 4-heart 5-heart 6-heart 7-heart 8-heart 9-heart 10-heart)
@deck.concat %w(2-club 3-club 4-club 5-club 6-club 7-club 8-club 9-club 10-club)
@deck.concat %w(2-spade 3-spade 4-spade 5-spade 6-spade 7-spade 8-spade 9-spade 10-spade)
@deck.concat %w(jack-diamond jack-heart jack-club jack-spade)
@deck.concat %w(queen-diamond queen-heart queen-club queen-spade)
@deck.concat %w(king-diamond king-heart king-club king-spade)
@deck.concat %w(ace-diamond ace-heart ace-club ace-spade)
@deck.concat %w(joker joker)
end
def join_game player_name
return false unless @hands.size < MAX_HANDS
player = {
name: player_name,
cards: []
}
@hands.push player
true
end
def deal
return false unless @hands.size > 0
@pool = @deck.shuffle
@hands.each {|player| player[:cards] = @pool.pop(5)}
true
end
def get_cards player_name
cards = 0
@hands.each do |player|
if player[:name] == player_name
cards = player[:cards].dup
break
end
end
cards
end
end
Note the call to the Sinatra library in line 2.
The card deck is built in lines 12 through 20 as one big array. Shuffled versions of this deck will be placed into the pool
array, from which cards will be dealt.
Players join the game using the join_game
method in line 23. Players provide a name, and the number of players is limited to four (MAX_HANDS
).
Cards are dealt using the deal
method in line 33. Essentially, the deck is shuffled, and the results are placed in the pool. Each player is then provided five cards from the pool.
Finally, players can get a look at their cards using the get_cards
method on line 40. They just need to identify themselves, and they will be provided a copy of their cards.
The second half of this file contains the exciting part: the implementation of the Sinatra REST API.
uno = UnoServer.new
###### Sinatra Part ######
set :port, 8080
set :environment, :production
get '/cards' do
return_message = {}
if params.has_key?('name')
cards = uno.get_cards(params['name'])
if cards.class == Array
return_message[:status] == 'success'
return_message[:cards] = cards
else
return_message[:status] = 'sorry - it appears you are not part of the game'
return_message[:cards] = []
end
end
return_message.to_json
end
post '/join' do
return_message = {}
jdata = JSON.parse(params[:data],:symbolize_names => true)
if jdata.has_key?(:name) && uno.join_game(jdata[:name])
return_message[:status] = 'welcome'
else
return_message[:status] = 'sorry - game not accepting new players'
end
return_message.to_json
end
post '/deal' do
return_message = {}
if uno.deal
return_message[:status] = 'success'
else
return_message[:status] = 'fail'
end
return_message.to_json
end
Note in line 53 that we instantiate a single Uno game as the uno
object. The Sinatra REST API in the subsequent lines will use this object.
Lines 57 and 58 allow us to determine the environment of this implementation. We wish to use TCP port 8080, and we set up a production environment. The production environment optimizes for speed, at the cost of providing less debugging information.
The REST API addresses the three main functions set up in the card game: the addition of players (join
), the dealing of cards (deal
) and the viewing of the cards (cards
). Each has been provided with its own URL.
Note the use of the GET
and POST
verbs in this implementation. When users want to see their cards, they issue a GET
command using the URL http://localhost:8080/cards. This GET
command includes a JSON structure that indicates the name of the player. The code simply calls the local uno
object’s get_cards
method to get the cards. If get_cards
returns with an array, the array is posted back to the user. If an array is not returned, the user is not a player in this game, so a “sorry” message is returned.
When players want to join a game, they issue a POST
command to the URL http://localhost::8080/join. If there is room in the current game, the user is allowed to join. If not, a “sorry” message is returned.
When players want to deal cards, they issue a POST
command to the URL http://localhost:8080/deal. Similar to the above scenarios, the local uno
object is sent a command to shuffle the deck and deal out five cards to each player. It can fail to deal if there are no players currently in the game.
In all the above cases, JSON-based values are returned to the players.
We’ll now look at the client code to see how the client side REST API implements the game environment.
# Ruby Uno Client
require 'json'
require 'rest-client'
class UnoClient
attr_reader :name
def initialize name
@name = name
end
def join_game
response = RestClient.post 'http://localhost:8080/join', :data => {name: @name}.to_json, :accept => :json
puts JSON.parse(response,:symbolize_names => true)
end
def get_cards
response = RestClient.get 'http://localhost:8080/cards', {:params => {:name => @name}}
puts response
end
def deal
response = RestClient.post 'http://localhost:8080/deal', :data =>{}.to_json, :accept => :json
puts response
end
end
As can be seen, the client-side communication is deceptively simple. We use the rest-client
Ruby library to create minimally functional methods that mimic the game-playing methods on the server.
We provide a name for this player when instantiating the UnoClient
class. Thereafter, we can join the game, deal cards, and get a copy of our cards.
Joining and dealing require POST
commands, as shown in lines 13 and 23.
Getting the cards requires a GET
command, as shown in line 18.
Note that we simply print out the response from the server, which is sent to us in JSON format. A fully developed game would obviously read the returned JSON structure and provide the corresponding results on the browser.
The listing below shows what happens when we instantiate clients. Using irb, five player objects for Bob, Carol, Ted, Alice and Ralph are created manually. Note the failure message when poor Ralph attempted to join the game. Also, notice that each player has been dealt a unique set of cards from the deck. All the action takes place on the server and only the results are sent back to the clients via the API. It appears that the Sinatra REST API is working!
> require './uno-client.rb'
> bob_uno = UnoClient.new 'bob'
> carol_uno = UnoClient.new 'carol'
> ted_uno = UnoClient.new 'ted'
> alice_uno = UnoClient.new 'alice'
> ralph_uno = UnoClient.new 'ralph'
> bob_uno.join_game
{:status=>"welcome"}
> carol_uno.join_game
{:status=>"welcome"}
> ted_uno.join_game
{:status=>"welcome"}
> alice_uno.join_game
{:status=>"welcome"}
> ralph_uno.join_game
{:status=>"sorry - game not accepting new players"}
> bob_uno.deal
{"status":"success"}
> bob_uno.get_cards
{"cards":["3-diamond","2-club","4-spade","9-diamond","8-spade"]}
> carol_uno.get_cards
{"cards":["9-club","5-spade","king-spade","6-club","7-heart"]}
> ted_uno.get_cards
{"cards":["4-heart","6-heart","2-spade","8-club","ace-heart"]}
> alice_uno.get_cards
{"cards":["4-club","7-club","3-club","king-heart","jack-club"]}
Summary
The REST architecture provides a very convenient mechanism to move data between clients and servers. This is especially important when you need to reduce the workload on your web server when dealing with highly dynamic data. As demonstrated in the example in this article, the Sinatra library can be used to implement the REST architecture in a very simple and straightforward manner.
Frequently Asked Questions (FAQs) about Implementing REST API with Sinatra
What is Sinatra and why is it used for implementing REST API?
Sinatra is a Domain Specific Language (DSL) for quickly creating web applications in Ruby. It is lightweight, flexible, and less complex than other Ruby frameworks like Rails. Sinatra is often used for implementing REST APIs because it allows for simple and straightforward routing and handling of HTTP requests, which is a core aspect of RESTful architecture.
How does Sinatra compare to other Ruby frameworks like Rails?
Sinatra is much more lightweight and less complex than Rails. It doesn’t come with many of the features that Rails has out of the box, but this also means it’s less bloated and more flexible. Sinatra is a great choice for smaller applications or for specific parts of larger applications, like implementing a REST API.
What is a REST API and why is it important?
A REST (Representational State Transfer) API (Application Programming Interface) is a set of rules and conventions for building and interacting with web services. It’s important because it allows different software applications to communicate with each other in a standardized way, making it easier to build complex, interconnected systems.
How do I get started with Sinatra?
To get started with Sinatra, you’ll first need to install Ruby and the Sinatra gem. Once you’ve done that, you can create a new Ruby file and require the Sinatra gem at the top. From there, you can start defining routes and handling HTTP requests.
What are the basic components of a Sinatra application?
A basic Sinatra application consists of a Ruby file that requires the Sinatra gem, defines routes, and handles HTTP requests. Each route corresponds to a URL pattern and an HTTP method (like GET or POST). When a request is made to a certain URL with a certain method, the corresponding route is triggered.
How do I define routes in Sinatra?
Routes in Sinatra are defined using methods that correspond to HTTP methods. For example, to define a route for a GET request to the “/hello” URL, you would write “get ‘/hello’ do … end” in your Sinatra application file. Inside the “do … end” block, you write the code that should be executed when the route is triggered.
How do I handle HTTP requests in Sinatra?
HTTP requests in Sinatra are handled within the “do … end” block of a route definition. This block of code is executed when the corresponding route is triggered. You can access the data from the request using the “params” hash, and you can send a response using methods like “erb” or “send_file”.
How do I send responses in Sinatra?
Responses in Sinatra are typically sent using methods like “erb” or “send_file”. The “erb” method renders a view template and sends it as the response, while the “send_file” method sends a file as the response. You can also simply return a string from the route block, and it will be sent as the response.
How do I use Sinatra to implement a REST API?
To use Sinatra to implement a REST API, you would define routes that correspond to the various actions of the API (like creating, reading, updating, and deleting resources). Each route would handle an HTTP request, perform the necessary action (like querying a database or modifying a file), and send a response.
What are some best practices for implementing a REST API with Sinatra?
Some best practices for implementing a REST API with Sinatra include keeping your routes RESTful (i.e., using the appropriate HTTP methods and URL patterns), handling errors gracefully, and providing clear and helpful responses. It’s also a good idea to keep your Sinatra application modular and well-organized, especially as it grows in complexity.
Dan Schaefer is a web designer living in Ventura County, California. He’s been a part of several startups, doing everything from engineering to sales to marketing.