DEV Community

Michael Lobman
Michael Lobman

Posted on

Water of Life - A Scotch Whisky API

Hello! I'm back with my first full-stack web application: "Water of Life," a Scotch whisky API using a React JS frontend and Object-Oriented Ruby, Active Record, and Sinatra on the backend.

For those unfamiliar with Scotch, it is essentially just whisky distilled and bottled in Scotland. Scotland itself is divided into regions, each region has many distilleries, and each distillery has many bottles. Sounds like a perfect dynamic to take advantage of Active Record macros!

The API is comprised of three tables: regions, distilleries, and bottles, each with a corresponding model that inherits from ActiveRecord::Base.

As for the associations between the models, a region instance has many distilleries, and a distillery instance has many bottles. The distillery model acts as the join class so that a region instance has many bottles through distilleries, and a bottle instance has one region through the distillery to which it belongs:

class Region < ActiveRecord::Base
    has_many :distilleries
    has_many :bottles, through: :distilleries
end
Enter fullscreen mode Exit fullscreen mode
class Bottle < ActiveRecord::Base
    belongs_to :distillery
    has_one :region, through: :distillery
end
Enter fullscreen mode Exit fullscreen mode

With these associations established, "Water of Life" harnesses the power of Active Record to provide useful, organized data the client requests through the routes established with Sinatra.

Yes, I did it Sinatra's way.

Not "My Way."

But trust me, "The Best is Yet to Come..."

The first and most comprehensive endpoint in the application controller is the get "/all" route:

get "/all" do
    regions = Region.all
    regions.to_json(include: { distilleries: { include: :bottles } })
end
Enter fullscreen mode Exit fullscreen mode

Simply, this returns all of the API's data, hierarchically structured by region, distillery, and bottle.

More dynamic routes, like get "/bottles/:id", take advantage of the params hash to locate and return the data for a specific bottle:

get "/bottles/:id" do
    bottle = Bottle.find(params[:id])
    bottle.to_json(include: { distillery: { include: :region } })
end
Enter fullscreen mode Exit fullscreen mode

Or even delete it from the database entirely:

delete "/bottles/:id" do
    bottle = Bottle.find(params[:id])
    bottle.destroy
    bottle.to_json(include: { distillery: { include: :region } })
end
Enter fullscreen mode Exit fullscreen mode

In both of these methods, the routes are not only returning information about the specific bottle instance but are "including" the data from its associated distillery and region as well:

bottle.to_json(include: { distillery: { include: :region }
Enter fullscreen mode Exit fullscreen mode

Such functionality is possible because of the associations established earlier with Active Record.

Finally, for one of the post routes on the API...

As a quasi well-adapted perfectionist, I want my API to be efficiently created and properly maintained, which includes new bottles being added to the database in the correct format. Many scotches have a multiple-word name: "Lagavulin 16 Year Old," "macallan double cask 12 year old," etc. I prefer the former, not the latter.

Of course, I could have imposed a regex validator on the frontend form, but users shouldn't inherit my problems (after all, I'm not their parent class!). There are Ruby gems that provide a method to "titleize" a string, but I wanted to write my own method to better understand the process involved:

def titleize(string)
      title_cased = string.split.map do |word|
        letters = word.split("")
        letters[0] = letters[0].upcase
        letters.join
      end
      title_cased.join(" ")
end
Enter fullscreen mode Exit fullscreen mode

And that, as they say, is that. My first API and full-stack web application. How far we've come...

Feeling thirsty? Check out "Water of Life" for details on your scotch of choice. And, if it's missing, please add it!

Discussion (0)