Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
143: Building a REST API with Sinatra
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 /booksroute that returns all books as a JSON array. - Create a
GET /books/:idroute that returns a single book. If the book doesn't exist, return a404status and a JSON error message. - Create a
POST /booksroute that accepts a JSON body containingtitleandauthor, adds it to your collection, and returns the new book with a201 Createdstatus. - Create a
DELETE /books/:idroute that removes the book and returns a204 No Contentstatus. - Ensure all responses are served with the
application/jsoncontent type using abeforefilter.
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.
There are no comments for now.