Uno! Use Sinatra to Implement a REST API

Share this article

Card game

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:

  1. join_game — allow a player to join the game
  2. deal — shuffle and deal cards to each player
  3. get_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:

  1. uno-server.rb — Contains the game-player logic as well as the Sinatra REST API server code
  2. uno-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 SchaeferDan Schaefer
View Author

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.

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week