Ruby Sinatra Tutorial

Advertisement

Advertisement

Introduction

Sinatra (http://sinatrarb.com/) is a minimalist web framework. If you are familiar with express.js, it was inspired by Sinatra. I have found it to be incredibly useful and fast to work with. It is known for being very simple and easy to use. This tutorial will cover some of the common tasks that I have used.

As always, refer to the official documentation for complete information and to learn more.

Installation

To install Sinatra, just use gem to install the package.

gem install sinatra

Some Linux distributions have a package already created for Sinatra. You might be able to install using your Linux package manager. For example:

# Ubuntu
sudo apt install ruby-sinatra
# Fedora
sudo dnf install rubygem-sinatra

Get Sinatra documentation

There are a couple options for viewing the Sinatra documentation. Here are instructions on getting the online and offline documentation.

Online documentation

You can view the official documentation online at http://sinatrarb.com/documentation.html.

Local gem documentation

You can view documentation locally by using the gem server.

gem server

Then visit the gem server in your browser. It defaults to http://localhost:8808/.

You will find a link to the Sinatra web page and the local rdocs. The local URL will be something similar to http://localhost:8808/doc_root/sinatra-2.0.5/.

Local system documentation

If you installed Sinatra using a Linux package manager like apt or dnf, it might have come with the documentation. Use your package manager to search

# In Ubuntu
apt-cache search sinatra
sudo apt install ruby-sinatra
dpkg -L ruby-sinatra
cd /usr/share/doc/ruby-sinatra
ls
sudo gunzip README.md.gz
less README.md

In Fedora, a separate package contains the documentation. After installing the rubygem-sinatra-doc package, the README.md file will be available in /usr/share/gems/gems/sinatra-*.

dnf search sinatra
sudo dnf install rubygem-sinatra
sudo dnf install rubygem-sinatra-doc
rpm -ql rubygem-sinatra-doc
less /usr/share/gems/gems/sinatra-2.0.3/REAMDE.md

Hello World

Here is an example that demonstrates just how simple Sinatra is to start using.

# hello.rb
require 'sinatra'

get '/' do
  'Hello, world!'
end

Then run the file and visit the page in the browser or use curl or wget to perform a test request. When starting the file it should announce what port is listening on in the console, which defaults to 4567.

ruby hello.rb
curl http://localhost:4567

Change default bind address and port

To modify the default listen address and port, can use the set function. By default, running the file will listen on localhost:4567. This is good for security as it only listens locally, but if you want to make it available publicly or on a different interface, you will want to override the listen address. Here is an example:

require 'sinatra'

set :bind, '0.0.0.0'
set :port, 9999

get '/' do
  "Now listening on all interfaces on port 9999"
end

For a production quality deployment, check out my tutorial: Deploy Ruby Rack Web Apps with uWSGI and Nginx.

Receive data

There are a number of ways to get data from a user, including:

  • Inspecting the HTTP request headers
  • Receiving a value as part of the URL
  • Receiving a value as a URL query parameter
  • Receiving a value from a POST form submission

We will look at each of these methods.

Inspect HTTP request headers

The request headers are stored as a Hash in the request environment variable, request.env. There are few special things to know about the request.env Hash though.

  • It contains all HTTP headers provided.
  • It also contains additional data like rack.url_scheme, rack.version, and rack.multithread.
  • Any HTTP headers provided will have the key name transformed:
    • Converted to all uppercase
    • Hyphens converted to underscores
    • Include a prefix of "HTTP_"
    • Examples:
    • "My-Header: my data" would come through as request.env['HTTP_MY_HEADER']
    • User agent comes through as request.env['HTTP_USER_AGENT']
    • Host comes through as request.env['HTTP_HOST']
require 'sinatra'
require 'pp'  # Pretty print

get '/' do
  puts 'Request headers:'
  puts request.env.class
  pp request.env
  'User agent: ' + request.env['HTTP_USER_AGENT']
end

You can test it out with curl http://localhost:4567/ -H "My-header: my data"

Routing with variables

Sinatra allows some complex URL path matching if necessary. We're going to look only at a simple example.

Some more advanced options include using regular exprssions to match complex patterns or matching conditions like a user-agent string. Those methods are out of scope for this tutorial and can be found documented in the official Sinatra documentation.

Here are a couple examples of taking values in the URL. The first option shows how to take a required parameter and access it via params object. The second one shows how to take an optional parameter and access the variable using the block declaration style.

require 'sinatra'

# :x is required for matching
# Try /x/testing
# Will not match /x because no trailing slash provided
# Will not match /x/ because :x param is missing
get '/x/:x' do
  "x is #{params['x']}"
end

# :y is optional with the ?
# Try /y/testing
# Will not match /y because trailing slash is required
# Will match /y/ since :y is optional
get '/y/:y?' do |y|
  "y is #{y}"
end

One thing to keep in mind with routing is that trailing slashes do matter. A route with a trailing slash is different than one without. The URLs must match exactly.

You can make the trailing slash optional with something like /path/?. Consider this possibility:

# Both the last slash and the :y parameter are optional
# Will match /y/testing
# Will match /y/ since :y is optional
# Will match /y since the last slash is optional
# Will match /yes since the last slash is optional
# Will not match /yes/ since there is no / after :y
# Will not match /yes/testing since routing is split by slashes
get '/y/?:y?' do |y|
   "y is #{y}"
end

The one interesting case with that last example is that it will match paths that starts with a "y", like /yes. The last slash is optional so it then treats the rest of the path after that as the :y parameter. However, adding any other slashes after the :y parameter though will create a different route that does not match. For example /yes matches but /yes/ will not.

Query parameters

Query parameters are also stored in the params object. For example, if you had the path http://localhost:4567/myurl?i=123, then the i query parameter would be accessible with params[:i].

Post data

Post parameters are also stored in to the same params variable. For example, if you had an HTML form with a text input like <input name="username" type="text" />, it would be accessible with params[:username] when submitted via POST.

Return data

Returning data to the requester can be done in several ways including:

  • Serving static files
  • Modifying or adding HTTP response headers
  • Changing response status code
  • Returning JSON
  • Returning an HTML template
  • Returning a specific file

Serve static files

By default, Sinatra serves static files from a public/ directory, if it exists. Simply put static files in to the public/ directory and they can be accessed directly. For example, if you had a file, public/test.txt then you could access it by visiting http://localhost:4567/test.txt.

You can override the location of the public directory by setting the :public_folder value like this:

require 'sinatra'

# Use the 'static/' directory relative to
# the location of the Ruby file
PUBLIC_DIR = File.join(File.dirname(__FILE__), 'static')

set :public_folder, PUBLIC_DIR

Modify HTTP response

You can return additional information to the reqeuster by modifying or adding HTTP header or changing the status code.

Modify HTTP response headers

To inspect the response headers, look at the headers object. To set the headers, call headers and pass in the values. It will set a header if it does not exist and override the header data if it does not exist.

require 'sinatra'

get '/' do
  puts headers
  headers "My-Header" => "My Data"
  puts headers
  headers "My-Header" => "Overriden.", "Extra-Header" => "Data..."
  puts headers
  'Done'
end

Modify status code

You can access the current status code in the status object. To modify the status code, call the status() function and give it the status code you want to return. For example, to specify an arbitrary status code of 999:

require 'sinatra'

get '/' do
  status 999
  puts status
  'Done'
end

Set mime type

Mime types, also known as content types, help the client or web browser understand what kind of data they are receiving in order to handle it properly. For example, if it is returning text/html it might handle it differently than receiving application/json text. You can set the content type to whatever you need and you can also create custom mime types for re-use and set a default mime type for all requests.

Create custom mime types
require 'sinatra'

configure do
  mime_type :my_mime_type, 'text/custom'
end

get '/' do
  content_type :my_mime_type
  # Or you can directly set a mime type like:
  # content_type 'text/mycustomtype'
  'Done'
end
Set default mime type

If you want to set application/json or some other mime type as the default for all requests, you can do that with the before hook.

require 'sinatra'

before do
  content_type 'application/custom'
end

# All responses will default to `application/custom`  type
get '/' do
  'Done'  
end

Return JSON

Sinatra has a built in mime type for JSON :json which translates to application/json. You could return JSON data by setting the mime type either explicitly in each request or by setting the default mime type for respones with configure as demonstrated in the previous section.

Manually return JSON

Manually by setting importing the Ruby JSON gem with require 'json', setting the response content-type to application/json and then calling .to_json() on your return object to convert it to JSON text.

require 'sinatra'
require 'json'

get '/' do
  content_type :json
  data = {"Hi" => "Hello", "Bye" => "Goodbye"}
  data.to_json
end

Or if you are building an API and you want it to use application/json by default:

require 'sinatra'
require 'json'

before do
  content_type :json
end

get '/' do
  data = {"Hi" => "Hello", "Bye" => "Goodbye"}
  data.to_json
end

If you wanted to take it one step further you could create a json function that handles both setting the content type and converting the object to JSON text.

require 'sinatra'
require 'json'

def json data_object
  content_type :json
  data_object.to_json
end

get '/' do
  json {"Hi" => "Hello", "Bye" => "Goodbye"}
end

Use sinatra-contrib's sinatra/json gem

There is a gem named sinatra-contrib that contains some extras like a convenience method for JSON. You have to install another dependency for this to work. The previous examples of returning JSON do not require any additional dependencies. You will need to install sinatra-contrib:

gem install sinatra-contrib

Then in your code you import sinatra/json and use their json function.

require 'sinatra'
require 'sinatra/json' # gem install sinatra-contrib

get '/' do
  json 'Hi' => 'Hello', :SomeKey => 'SomeValue'
end

Return a file

Read the previous sections about creating custom mime types (content types), setting mime types setting default mime types. mime types.

You can use send_file to return static files. The send_file function will automatically determine the mime type and content length. You can specify them if necessary and also set the last-modified time and status if necessary as parameters to the send_file function.

require 'sinatra'

get '/' do
  # Returns `test.txt` from the current directory
  send_file 'test.txt'
end

If you want the client's web browser to treat the file as a download as opposed to something that should be displayed in the browser, you want to set the Content-Disposition header to attachment. You can do this by either calling the attachment function somewhere in your handler, or pass :disposition => 'attachment' to the send_file function.

require 'sinatra'

get '/' do
  attachment
  send_file 'test.txt'
end

Or:

require 'sinatra'

get '/' do
  send_file 'test.txt', :disposition => 'attachment'
end

You can also set the name of the downloaded file separately if desired:

require 'sinatra'

get '/' do
  send_file 'test.txt', :disposition => 'attachment', :filename => 'custom.txt'
end

Templates

Sinatra supports templates out of the box. It can handle HAML, ERB, and Markdown templates among dozens others. Personally I prefer ERB as it is basically regular HTML with the ability to drop in Ruby code, very similar to PHP files. Using ERB templates also does not require any additional dependencies.

Sinatra assumes a default template directory named views/.

Layout templates

Sinatra will look for a default layout of layout.erb and checks for its existence. If a layout exists it will default to that. If a layout.erb does not exist, and a layout is not explicitly passed to the erb function, then no layout will be used to encapsulate the final template.

views/layout.erb example:

<%# Optional default layout file `views/layout.erb` %>
<html>
<body>

  <%= yield %>

</body>
</html>

All content from the sub-template will end up in the yield block.

Return a template

You can return a template using the following syntax. This will look for templates views/index.erb.

require 'sinatra'

get '/' do
  # Looks for `views/index.erb`
  erb :index
  # Templates in subdirectories can be reached like:
  # erb :'account/index'
end

The views/index.erb template can contain HTML like this:

<h1>Welcome</h1>
<p>This is my index template.</p>

Pass variables to templates

This example shows how to pass variables to a template.

require 'sinatra'

get '/hello' do
  erb :hello, :locals => {:greeting => "Howdy"}
end

In views/hello.erb you can use <%= => tags to echo a variable:

<p>
  <%= greeting %>
</p>

You can also use regular Ruby statements inside <% %> blocks. For example if an iterable object was passed to the ERB template you could do:

<% my_iterable.each do |item| %>
  <p>
    <%= item %>
  </p>
<% end %>

One important thing to keep in mind is that any data provided by a user could be malicious. You should always escape any HTML provided to lower cross-site scripting and cross-site request forgery risks. You can use the Rack::Utils.escape_html() function. It will be available after calling require 'sinatra'. For more tips on HTML escaping including how to set up a convenient helper function and turn on auto-escape with Erubis, check out the official Sinatra FAQ.

Session data

Sessions are a way of persisting data for a user that can be accessed across multiple requests. A common use for sessions is to store a token used to validate authentication on the server.

To use them in Sinatra you simply have to enable them and then the session object becomes available. It automatically handles the overhead of managing sessions. It uses Rack under the hood to manage sessions and cookies, which you can utilize directly if desired. It uses serialized, signed and encrypted cookie data rather than a database to maintain the session information. Users cannot read the data since it is encrypted, and they cannot tamper with it because it is signed.

require 'sinatra'

enable :sessions

get '/set' do
  session[:mydata] = 'My data'
  'Complete'
end

get '/get' do
  "Data: #{session[:mydata]}"
end

Redirects

Redirecting is as simple as calling the redirect() function. Another useful function provided is the to() function. Using to() will generate a fully qualified URL based on the current server and protocol. For example, to('/') in the default setup would return the string http://localhost:4567/. By default it uses a 302 temporary redirect code.

require 'sinatra'

get '/' do
  'Welcome back.'
end

get '/goaway' do
  # Optionally, use the to() function to
  # generate a fully qualified URL (http://localhost:4567/)
  # redirect to('/')  

  # Or simply go back to previous page
  # redirect back

  # To specify the return code
  # redirect '/', 301  # Redirect with 301 permanent

  redirect '/'
end

Authentication

Rack provides a way to protect your application with basic HTTP authentication. You perform your authentication check and then return true or false depending on whether you want to grant access to the username and password combination.

require 'sinatra'

use Rack::Auth::Basic, "Authorization Required" do |user, pass|
  # Perform some kind of check and return true/false
  access_permitted = false
  if user == "admin" and pass == "admin"
    access_permitted = true
  end
  access_permitted
end

get '/' do
  "Access granted"
end

Logging

Sinatra provides a logger object that is available in each request. By default it outputs info, warn, and error messages, but not debug. Consult the official documentation for instructions on configuring log levels. Using different log levels allows you to tune the verbosity of logging and makes certain events more easily greppable.

require 'sinatra'

get '/' do
  logger.info 'Informational log'
  logger.warn 'Warning log'
  logger.error 'Error!'
  logger.debug 'Debug message'
end

Deploy Sinatra to production

I have written a dedicated tutorial to deploying Ruby Rack applications using nginx and uWSGI:

Deploy Ruby Rack Web Apps with uWSGI and Nginx.

Conclusion

After reading this you should have a solid grasp of Sinatra and its fundamentals to make web applications. You sould know how to install and get started as well as where to go to learn more about the advanced features it provides.

References

Advertisement

Advertisement