Skip to Content
Course content

143: Building a REST API with Sinatra

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to start this lesson with a snippet of code that looks perfectly fine at first glance. Imagine you're building a simple API to track a collection of vintage vinyl records. You've set up your Sinatra app, and you've written a POST route to add new records to your database.

require 'sinatra'
require 'json'

$records = []

post '/records' do
  new_record = {
    title: params[:title],
    artist: params[:artist]
  }
  $records << new_record
  status 201
  { message: "Record added!", record: new_record }.to_json
end

You send a request using a tool like Postman or cURL with a JSON body: {"title": "A Love Supreme", "artist": "John Coltrane"}. But when you check your $records array, you find that you've just added a record where the title and artist are both nil. You're staring at the code, wondering why params is empty even though you clearly sent the data.

The Mystery of the Missing Params

Here is the catch: Sinatra's params hash is designed to handle standard form-encoded data (the kind you get from a basic HTML <form>). When you send a request with the header Content-Type: application/json, Sinatra doesn't automatically parse that raw JSON string into the params hash. The data is actually sitting there in the request body, but it's just a long string of text that Sinatra hasn't touched.

Parsing the JSON Body Manually

To fix this, we have to reach into the request body, read the raw string, and parse it using the JSON library. I usually handle this by creating a helper method or doing it directly inside the route. Here is the corrected version:

post '/records' do
  # Read the raw body and parse it into a Ruby hash
  request_payload = JSON.parse(request.body.read)
  
  new_record = {
    title: request_payload['title'],
    artist: request_payload['artist']
  }
  $records << new_record
  
  content_type :json
  status 201
  { message: "Record added!", record: new_record }.to_json
end

Notice two things here. First, we used request.body.read to get the raw JSON. Second, I added content_type :json. Without that, Sinatra defaults to text/html, and any client consuming your API might get confused about how to handle the response.

Mapping HTTP Verbs to Resources

Now that we can actually get data into our app, let's talk about the "REST" part of a REST API. REST is less of a strict rulebook and more of a set of conventions. The goal is to use HTTP verbs to describe the action being taken on a "resource" (in our case, a record).

  • GET /records: Fetch the entire list.
  • GET /records/:id: Fetch one specific record.
  • POST /records: Create a new record.
  • PUT /records/:id: Update an existing record.
  • DELETE /records/:id: Remove a record.

When you're building these, remember that :id is a named parameter. Sinatra will put whatever value is in that spot of the URL into the params hash for you automatically.

Handling State and Response Codes

A common mistake I see is returning a 200 OK for everything. If you're building an API that other developers will use, you need to be more descriptive. If a user tries to GET a record with an ID that doesn't exist, don't just return an empty object; return a 404 Not Found.

Here is how I would structure a full, minimal RESTful controller for our records:

require 'sinatra'
require 'json'

$records = {
  1 => { title: "Kind of Blue", artist: "Miles Davis" },
  2 => { title: "Blue Train", artist: "John Coltrane" }
}

before do
  content_type :json
end

get '/records' do
  $records.values.to_json
end

get '/records/:id' do
  record = $records[params[:id].to_i]
  return halt 404, { error: "Record not found" }.to_json unless record
  record.to_json
end

put '/records/:id' do
  record = $records[params[:id].to_i]
  return halt 404, { error: "Record not found" }.to_json unless record
  
  payload = JSON.parse(request.body.read)
  record.merge!(payload)
  record.to_json
end

delete '/records/:id' do
  $records.delete(params[:id].to_i)
  status 204 # No Content
end

I used a before filter here to set the content_type for every single request. It saves you from typing it in every route. I also used halt, which is a great Sinatra method to stop the execution of a route immediately and return a specific status code and body.




📋 Practical Task

Build a Personal Book Inventory API

Your task is to create a small REST API using Sinatra to manage a collection of books. You should use an in-memory Hash to store your data to keep things simple.

Requirements:

  • Create a GET /books route that returns all books as a JSON array.
  • Create a GET /books/:id route that returns a single book. If the book doesn't exist, return a 404 status and a JSON error message.
  • Create a POST /books route that accepts a JSON body containing title and author, adds it to your collection, and returns the new book with a 201 Created status.
  • Create a DELETE /books/:id route that removes the book and returns a 204 No Content status.
  • Ensure all responses are served with the application/json content type using a before filter.

Testing Tip: Use a tool like Postman, Insomnia, or the curl command in your terminal to test your POST and DELETE requests, as your web browser can only perform GET requests by default.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.