Created by Stan on 10-04-2023
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 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 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
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
Create a config.ru
file to run the Rack application:
# config.ru require_relative 'bookstore_api' run BookstoreAPI.new
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
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.
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.
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
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.
Coding
Posted on 07 Apr, 2023Coding
Posted on 07 Apr, 2023Coding
Posted on 07 Apr, 2023