In this article, we will build a simple Bookstore API using Rack and PostgreSQL. Rack is a lightweight web framework for Ruby that provides a minimal interface between web servers and Ruby applications. We will use PostgreSQL as our database backend.
We will create an API with two endpoints: a GET endpoint for retrieving all books and a POST endpoint for adding new books. Additionally, we will provide scripts to create and delete the bookstore database.
Prerequisites:
- Ruby
- PostgreSQL
First, create a new directory for your project:
mkdir bookstore_api
cd bookstore_api
Create a Gemfile
to manage your project's dependencies:
# Gemfile
source 'https://rubygems.org'
gem 'rack'
gem 'pg'
gem 'sequel'
gem 'rack-cors'
gem 'json'
Run bundle install
to install the required gems.
Create the database
Create a new file called db.rb
to connect to the PostgreSQL database:
# db.rb
require 'sequel'
DB = Sequel.connect('postgres://postgres:postgres@localhost/bookstore')
Replace 'postgres:postgres' with your PostgreSQL credentials.
Create the API
Create a new file called bookstore_api.rb
and define the BookstoreAPI class:
require 'rack'
require 'json'
require_relative 'db'
class BookstoreAPI
# ...
end
Implement the call method to handle incoming requests:
def call(env)
request = Rack::Request.new(env)
response = Rack::Response.new
case request.path_info
when '/books'
handle_books_request(request, response)
else
response.status = 404
response.write('Not found')
end
response.finish
end
Implement the GET and POST endpoints
def handle_books_request(request, response)
case request.request_method
when 'GET'
get_books(response)
when 'POST'
create_book(request, response)
else
response.status = 405
response.write('Method not allowed')
end
end
def get_books(response)
books = DB[:books].all
response.write(books.to_json)
end
def create_book(request, response)
# ...
end
Implement the create_book
method to handle book creation:
def create_book(request, response)
book_data = JSON.parse(request.body.read)
title = book_data['title']
author = book_data['author']
published_date = book_data['published_date']
price = book_data['price']
if title && author
book_id = DB[:books].insert(title: title, author: author, published_date: published_date, price: price)
response.status = 201
response.write({ id: book_id }.to_json)
else
response.status = 400
response.write('Invalid book data')
end
end
Set up Rack
Create a config.ru
file to run the Rack application:
# config.ru
require_relative 'bookstore_api'
run BookstoreAPI.new
Create scripts to manage the database
Add a script to delete the database in delete_db.rb
:
# delete_db.rb
require 'pg'
def delete_database
conn = PG.connect(dbname: 'postgres', user: 'postgres', password: 'postgres', host: 'localhost')
result = conn.exec("SELECT 1 FROM pg_database WHERE datname = 'bookstore';")
unless result.any?
puts "Database doesn't exist"
return
end
conn.exec("DROP DATABASE IF EXISTS bookstore;")
puts "Db successfully dropped"
conn.close
end
delete_database
Running the scripts
Run the setup_db
script in order to create the bookstore
database and the books
table.
ruby setup_db.rb
You can also run the delete_db.rb
script if you wish to delete the database. Additionally, you can add to the delete_db.rb
script if you only wish to get rid of individual tables.
Run the API
To start the Rack server, run the following command:
rackup
The API should now be running on http://localhost:9292
. You can use a tool like Postman or curl to test your endpoints.
Your POST
request should look as follows:
{
"title": "Animal Farm",
"author": "George Orwell",
"published date": "27-08-1945",
"price": 22.5
}
When you click on the GET
endpoint for /posts
, you should be able to set an array that includes the newly created record.
Let's refactor
As we can see, our create_book
method in the bookstore api has gone on a bit long. Let's clean this up a bit.
To begin, let's create a method to parse the request data.
def parse_request_body(request)
JSON.parse(request.body.read)
end
We'll add another method to check if the request data is valid. Our API expects the title
and author
attributes to be present.
def valid_book_data?(book_data)
book_data['title'] && book_data['author']
end
We will then insert the request data into the database using Sequel.
def insert_book_into_database(book_data)
DB[:books].insert(
title: book_data['title'],
author: book_data['author'],
published_date: book_data['published_date'],
price: book_data['price']
)
end
Finally, we will create methods to send valid / invalid responses.
def send_created_response(response, book_id)
response.status = 201
response.write({ id: book_id }.to_json)
end
def send_invalid_book_data_response(response)
response.status = 400
response.write('Invalid book data')
end
Our final code for the bookstore_api
should look like this.
require 'rack'
require 'json'
require_relative 'db'
class BookstoreAPI
def call(env)
request = Rack::Request.new(env)
response = Rack::Response.new
case request.path_info
when '/books'
handle_books_request(request, response)
else
response.status = 404
response.write('Not found')
end
response.finish
end
private
def handle_books_request(request, response)
case request.request_method
when 'GET'
get_books(response)
when 'POST'
create_book(request, response)
else
response.status = 405
response.write('Method not allowed')
end
end
def get_books(response)
books = DB[:books].all
response.write(books.to_json)
end
def create_book(request, response)
book_data = parse_request_body(request)
if valid_book_data?(book_data)
book_id = insert_book_into_database(book_data)
send_created_response(response, book_id)
else
send_invalid_book_data_response(response)
end
end
def parse_request_body(request)
JSON.parse(request.body.read)
end
def valid_book_data?(book_data)
book_data['title'] && book_data['author']
end
def insert_book_into_database(book_data)
DB[:books].insert(
title: book_data['title'],
author: book_data['author'],
published_date: book_data['published_date'],
price: book_data['price']
)
end
def send_created_response(response, book_id)
response.status = 201
response.write({ id: book_id }.to_json)
end
def send_invalid_book_data_response(response)
response.status = 400
response.write('Invalid book data')
end
end
Let's write some specs
Add the following gems to your Gemfile
gem 'rspec'
gem 'rack-test'
Add a bookstore_api_spec.rb
file to test your API:
require 'rspec'
require 'rack/test'
require_relative 'bookstore_api'
RSpec.describe BookstoreAPI do
include Rack::Test::Methods
def app
BookstoreAPI.new
end
describe 'GET /books' do
before do
# Clear the books table and insert test data
DB[:books].truncate
DB[:books].insert(title: 'The Catcher in the Rye', author: 'J.D. Salinger', published_date: '1951-07-16', price: 10.99)
DB[:books].insert(title: 'To Kill a Mockingbird', author: 'Harper Lee', published_date: '1960-07-11', price: 12.99)
end
it 'returns a list of books' do
get '/books'
expect(last_response).to be_ok
books = JSON.parse(last_response.body)
expect(books.size).to eq(2)
end
end
describe 'POST /books' do
let(:valid_book_data) do
{
'title' => '1984',
'author' => 'George Orwell',
'published_date' => '1949-06-08',
'price' => 14.99
}
end
let(:invalid_book_data) do
{
'title' => 'The Great Gatsby',
'published_date' => '1925-04-10',
'price' => 10.99
}
end
it 'creates a book with valid data' do
post '/books', valid_book_data.to_json, { 'CONTENT_TYPE' => 'application/json' }
expect(last_response.status).to eq(201)
book_id = JSON.parse(last_response.body)['id']
expect(book_id).not_to be_nil
book = DB[:books].where(id: book_id).first
expect(book[:title]).to eq(valid_book_data['title'])
expect(book[:author]).to eq(valid_book_data['author'])
end
it 'returns an error with invalid data' do
post '/books', invalid_book_data.to_json, { 'CONTENT_TYPE' => 'application/json' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('Invalid book data')
end
end
end
Run the specs with the following command.
rspec .
All specs should be passing.
Our final tree structure should look as follows. Only 8 files for an API - pretty neat, right!?!
├── Gemfile
├── Gemfile.lock
├── bookstore_api.rb
├── bookstore_api_spec.rb
├── config.ru
├── db.rb
├── delete_db.rb
└── setup_db.rb
In this article, we built a simple Bookstore API using Rack and PostgreSQL. We created two endpoints for getting and adding books and provided scripts to manage the database. With this foundation, you can extend the API to include more functionality like updating or deleting books, and even add authentication.
Top comments (0)