Basics of Building a CRUD API with Ruby Sinatra
July 24, 2021
Ruby Sinatra is probably the second most popular web framework in the Ruby ecosystem, second to the behemoth of Ruby on Rails. Ruby is a more minimalist framework like Javascripts, Express or Pythons, Flask.
Before this tutorial make sure to have Ruby installed.
Summary of RESTful Convention
THe restful convention gives us a blueprint of making the basic routes for CRUD (Create, Read, Update, Delete) functionality in a uniform way.
API Restful Routes
Name of Route | Request Method | Endpoint | Result |
---|---|---|---|
Index | GET | /model |
returns list of all items |
Show | GET | /model/:id |
returns item with matching id |
Create | Post | /model |
creates a new item, returns item or confirmation |
Update | Put/Patch | /model/:id |
Updated item with matching ID |
Destroy | Delete | /model/:id |
Deletes item with matching ID |
If we weren’t build an API but instead rendering pages on the server there would be two additional routes. New, which renders a page with a form to create a new object, submitting the form triggers the create route. Edit, which renders a page with a form to edit an existing object, submitting the form triggers the Update route.
Since we are building an api, Edit and New aren’t necessary as the burden of collecting the information to submit to the Create and Update route will be on whoever builds the applications that consume the API. (Frontend Applications built in frameworks)
API in Sinatra
- create a new folder and navigate your terminal to it
- inside the folder create a file called
server.rb
- install sinatra
gem install sinatra
Our Model
What we will do is instead of using a database we will just use an array of hashes. So let’s create an initial data set representing blog posts.
server.rb
require 'sinatra'
## Model/Dataset
posts = [{title: "First Post", body: "content of first post"}]
The Index Route
The Index Route displays all items of the model. So this route should return all posts when a get request is sent to “/posts”.
server.rb
require 'sinatra'
## Model/Dataset
posts = [{title: "First Post", body: "content of first post"}]
## Index route
get '/posts' do
# Return all the posts as JSON
return posts.to_json
end
- run the server
ruby server.rb
- go to localhost:4567/posts in your browser
Pretty easy, right!
The Show Route
The show route allows you to see one of the items based on an ID, since we’re using an array instead of a database we’ll pretend the array index is the id number.
require 'sinatra'
## Model/Dataset
posts = [{title: "First Post", body: "content of first post"}]
## Index route
get '/posts' do
# Return all the posts as JSON
return posts.to_json
end
## Show Route
get '/posts/:id' do
# return a particular post as json based on the id param from the url
# Params always come to a string so we convert to an integer
id = params["id"].to_i
return posts[id].to_json
end
- run the server and test out
/posts/0
The Create Route
The create route usually receives information in the request body and creates a new data entry. This would be a post request to /posts
.
To get the request body we define a custom method earlier in the file, make sure to add it.
require 'sinatra'
## Model/Dataset
posts = [{title: "First Post", body: "content of first post"}]
## Custom Method for Getting Request body
def getBody (req)
## Rewind the body in case it has already been read
req.body.rewind
## parse the body
return JSON.parse(req.body.read)
end
## Index route
get '/posts' do
# Return all the posts as JSON
return posts.to_json
end
## Show Route
get '/posts/:id' do
# return a particular post as json based on the id param from the url
# Params always come to a string so we convert to an integer
id = params["id"].to_i
return posts[id].to_json
end
## Create Route
post '/posts' do
# Pass the request into the custom getBody function
body = getBody(request)
# create the new post
new_post = {title: body["title"], body: body["body"]}
# push the new post into the array
posts.push(new_post)
# return the new post
return new_post.to_json
end
- using a tool like postman make a post request to /posts, make sure to include a proper json body
{
"title": "Another Post",
"body":"content in the new post"
}
The Update Route
The update route should receive the id of the item to updated in the url and then update it using the data passed via the request body. This is typically done via a Put and/or Patch request to /posts/:id.
require 'sinatra'
## Model/Dataset
posts = [{title: "First Post", body: "content of first post"}]
## Custom Method for Getting Request body
def getBody (req)
## Rewind the body in case it has already been read
req.body.rewind
## parse the body
return JSON.parse(req.body.read)
end
## Index route
get '/posts' do
# Return all the posts as JSON
return posts.to_json
end
## Show Route
get '/posts/:id' do
# return a particular post as json based on the id param from the url
# Params always come to a string so we convert to an integer
id = params["id"].to_i
return posts[id].to_json
end
## Create Route
post '/posts' do
# Pass the request into the custom getBody function
body = getBody(request)
# create the new post
new_post = {title: body["title"], body: body["body"]}
# push the new post into the array
posts.push(new_post)
# return the new post
return new_post.to_json
end
## Update Route
put '/posts/:id' do
# get the id from params
id = params["id"].to_i
# get the request body
body = getBody(request)
#update the item in question
posts[id][:title] = body["title"]
posts[id][:body] = body["body"]
#return the updated post
return posts[id].to_json
end
- make a put request to /posts/0 and include a proper json body to test
The Destroy Route
The destroy route takes in an id of an item to be deleted in the url and deletes it. This is done by making a delete request to /posts/:id
require 'sinatra'
## Model/Dataset
posts = [{title: "First Post", body: "content of first post"}]
## Custom Method for Getting Request body
def getBody (req)
## Rewind the body in case it has already been read
req.body.rewind
## parse the body
return JSON.parse(req.body.read)
end
## Index route
get '/posts' do
# Return all the posts as JSON
return posts.to_json
end
## Show Route
get '/posts/:id' do
# return a particular post as json based on the id param from the url
# Params always come to a string so we convert to an integer
id = params["id"].to_i
return posts[id].to_json
end
## Create Route
post '/posts' do
# Pass the request into the custom getBody function
body = getBody(request)
# create the new post
new_post = {title: body["title"], body: body["body"]}
# push the new post into the array
posts.push(new_post)
# return the new post
return new_post.to_json
end
## Update Route
put '/posts/:id' do
# get the id from params
id = params["id"].to_i
# get the request body
body = getBody(request)
#update the item in question
posts[id][:title] = body["title"]
posts[id][:body] = body["body"]
#return the updated post
return posts[id].to_json
end
## Destroy Route
delete '/posts/:id' do
# get the id from params
id = params["id"].to_i
# delete the item
post = posts.delete_at(id)
# return the deleted item as json
return post.to_json
end
- test it out by making a delete request to /posts/0
Conclusion
You’ve just made a full crud API using Ruby Sinatra. The next step is to try to implement a database so that the data persists. There are many databases and corresponding libraries to use so go out and experiment.
Bonus (CORS)
Here is a version of the build with the extra code for setting CORS headers:
require "sinatra"
posts = [{title: "First Post", body: "content of first"}]
## custom method for getting Request body
def getBody(req)
## rewind the body
req.body.rewind
## parse the body
return JSON.parse(req.body.read)
end
## Function for setting CORS headers
def setCors
## create hash of the cors headers
cors_headers = {
## URLS allowed to make request (* means any)
"Access-Control-Allow-Origin" => "*",
## Request methods allowed
"Access-Control-Allow-Methods" => "*",
## Headers allowed in requests
"Access-Control-Allow-Headers" => "*",
## Max length of a request before timing out in seconds
"Access-Control-Max-Age" => "86400"
}
## set the headers
headers(cors_headers)
end
## Index Route
get '/posts' do
#Return all the posts as json
setCors()
return posts.to_json
end
## Show Route
get "/posts/:id" do
setCors()
id = params["id"].to_i ## the string "0" turning to 0
return posts[id].to_json
end
## Create Route
post '/posts' do
setCors()
## get the request body
body = getBody(request)
## create the new post
new_post = {title: body["title"], body: body["body"]}
## push the new post into the array
posts.push(new_post)
## return the post as json
return new_post.to_json
end
## Update Route
put "/posts/:id" do
setCors()
## get the id from params
id = params["id"].to_i
## get the body
body = getBody(request)
## go update the thing
posts[id][:title] = body["title"]
posts[id][:body] = body["body"]
## return the updated post
return posts[id].to_json
end
## Destroy Route
delete "/posts/:id" do
setCors()
## Get the id from params
id = params["id"].to_i
## Delete the item
post = posts.delete_at(id)
## return the deleted item as json
return post.to_json
end