Introduction
I am going to show you how to build a web service to create, read, update and delete movies with Ruby, Sinatra and Datamapper.
This tip assumes you know how to execute an HTTP request and that you have basic knowledge about Ruby.
Technologies Used
To create this new service, we are going to use 3 useful tools that are available for free on the internet.
Ruby
The language that everybody is talking about, specially since the rails framework was released.
Sinatra
A Lightweight Web Framework for the Ruby programming language.
Datamapper
It's an Object Relational Mapper written in Ruby that allows you to access the database without pain.
Project Structure
The Sinatra framework does not have an official project structure, which means that you can play around with it and choose the structure you like the most.
With Sinatra, you can build from a simple one file website or service to a big and structured project with modules and almost everything you can think of.
The structure I created for the project is this:
The important files in the project are:
models/movie.rb
This file is a Datamapper
model, which means that there will be a table in the database containing the structure described in the file:
# encoding: UTF-8
class Movie
include DataMapper::Resource
property :id, Serial
property :title, String
property :director, String
property :synopsis, Text
property :year, Integer
end
routes/movies.rb
The file where we define the entry points of our application. For building service address, I usually follow the rules given below:
- Verbs are bad in routes, nouns are always welcome. good example: http get /movies/:id, bad example: http get /getmoviesbyid/:id
- HTTP headers are your friends, use them for changing the response format and content-type, at the end of the day, you will have clean URLs for the service you build.
- Names in routes must be plural.
HTTP VERB | RESOURCE | ACTION |
get | /api/movies | get all movies |
get | /api/movies/:id | get a single movie by id |
post | /api/movies | create a new movie |
put | /api/movies/:id | update a specific movie by id |
delete | /api/movies/:id | delete a movie by id |
# encoding: UTF-8
get '/api/movies' do
format_response(Movie.all, request.accept)
end
get '/api/movies/:id' do
movie ||= Movie.get(params[:id]) || halt(404)
format_response(movie, request.accept)
end
post '/api/movies' do
body = JSON.parse request.body.read
movie = Movie.create(
title: body['title'],
director: body['director'],
synopsis: body['synopsis'],
year: body['year']
)
status 201
format_response(movie, request.accept)
end
put '/api/movies/:id' do
body = JSON.parse request.body.read
movie ||= Movie.get(params[:id]) || halt(404)
halp 500 unless movie.update(
title: body['title'],
director: body['director'],
synopsis: body['synopsis'],
year: body['year']
)
format_response(movie, request.accept)
end
delete '/api/movies/:id' do
movie ||= Movie.get(params[:id]) || halt(404)
halt 500 unless movie.destroy
end
helpers/response_format.rb
This file will be in change of formatting the HTTP response in different formats depending on the HTTP Accept
header.
# encoding: UTF-8
require 'sinatra/base'
module Sinatra
module ResponseFormat
def format_response(data, accept)
accept.each do |type|
return data.to_xml if type.downcase.eql? 'text/xml'
return data.to_json if type.downcase.eql? 'application/json'
return data.to_yaml if type.downcase.eql? 'text/x-yaml'
return data.to_csv if type.downcase.eql? 'text/csv'
return data.to_json
end
end
end
helpers ResponseFormat
end
This custom Sinatra helper allows us to respond in different formats and give the clients to change the response format they want.
main.rb
Finally, we have the file that puts all the projects together and makes it possible to run the application.
# encoding: UTF-8
require 'json'
require 'sinatra'
require 'data_mapper'
require 'dm-migrations'
configure :development do
DataMapper::Logger.new($stdout, :debug)
DataMapper.setup(
:default,
'mysql://root:12345@localhost/sinatra_service'
)
end
configure :production do
DataMapper.setup(
:default,
'postgres://postgres:12345@localhost/sinatra_service'
)
end
require './models/init'
require './helpers/init'
require './routes/init'
DataMapper.finalize
Running the Project
In order to be able to run the project, we need to follow a set of steps:
- Install the ruby gem bundler [sudo] gem install bundler
- Navigate to the project directory and run bundle install, you should see something like:
- Run rake migrate so the database structure gets created auto-magically.
- Run ruby main.rb
If you followed the steps above, you should now be serving a brand new web service.
Consuming the Web Service
For consuming the web service, you can use whatever you want, you can use curl, postman, or any other HTTP client library for your favorite programming language.
First of all, we need to create some records to test the rest of the service, so let's create some movies by executing an HTTP post request to the server.
HTTP POST
http://localhost:4567/api/movies
Request body:
{
"title": "The Shawshank Redemption",
"director": "Frank Darabont",
"synopsis": "Two imprisoned men bond over a number of years,
finding solace and eventual redemption through acts of common decency. ",
"year": 1994
}
http://localhost:4567/api/movies
Request body:
{
"title": "The Dark Knight",
"director": "Christopher Nolan",
"synopsis": "When Batman, Gordon and Harvey Dent
launch an assault on the mob, they let the clown out of the box,
the Joker, bent on turning Gotham on itself and bringing any heroes down to his level.",
"year": 2008
}
HTTP GET
http://localhost:4567/api/movies
[
{
"id": 1,
"title": "The Shawshank Redemption",
"director": "Frank Darabont",
"synopsis": "Two imprisoned men bond over a number of years,
finding solace and eventual redemption through acts of common decency. ",
"year": 1994
},
{
"id": 2,
"title": "The Dark Knight",
"director": "Christopher Nolan",
"synopsis": "When Batman, Gordon and Harvey Dent launch an
assault on the mob, they let the clown out of the box, the Joker,
bent on turning Gotham on itself and bringing any heroes down to his level.",
"year": 2008
}
]
http://localhost:4567/api/movies/2
{
"id": 2,
"title": "The Dark Knight",
"director": "Christopher Nolan",
"synopsis": "When Batman, Gordon and Harvey Dent launch an assault
on the mob, they let the clown out of the box, the Joker,
bent on turning Gotham on itself and bringing any heroes down to his level.",
"year": 2008
}
HTTP PUT
http://localhost:4567/api/movies/2
Request body:
{
"title": "The Dark Knight",
"director": "Christopher Nolan",
"synopsis": "Updated synopsis",
"year": 2008
}
HTTP DELETE
http://localhost:4567/api/movies/2
Changing the Response Format
It is possible to change the response format by specifying the Accept header, for instance, if you want to get XML back from the server, you need to specify the Accept: text/xml header in the request:
XML Response:
curl -H "Accept: text/xml" http://localhost:4567/api/movies
="1.0"
<movies type="array">
<movie>
<id type="integer">1</id>
<title>The Shawshank Redemption</title>
<director>Frank Darabont</director>
<synopsis>Two imprisoned men bond over a number of years,
finding solace and eventual redemption through acts of common decency.
</synopsis>
<year type="integer">1994</year>
</movie>
</movies>
JSON Response (default):
[
{
"id": 1,
"title": "The Shawshank Redemption",
"director": "Frank Darabont",
"synopsis": "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency. ",
"year": 1994
}
]
YAML Response:
curl -H "Accept: text/x-yaml" http://localhost:4567/api/movies
---
- !ruby/DataMapper,1.2.0.rc1:Movie
id: 1
title: The Shawshank Redemption
director: Frank Darabont
synopsis: Two imprisoned men bond over a number of years, finding solace and eventual
redemption through acts of common decency.
year: 1994
CSV Response:
curl -H "Accept: text/csv" http://localhost:4567/api/movies
1,The Shawshank Redemption,Frank Darabont,"Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency. ",1994
Source Code
The source code of this project is available in a public repository at Bitbucket.
You can get it if you install git and run:
git clone https://bitbucket.org/crojasaragonez/sinatra_service.git
Points of Interest
Writing web services with Sinatra is really fast, you can create them for test and production purposes. This is just a minimum example of what you can build with Sinatra, there's a lot of good documentation on their site that is worth seeing.