Photo by Shiromani Kant on Unsplash
Building a Reading Log App with the Google Books API and Ruby on Rails - Part 1
Using Faraday to make successful HTTP requests to an external API
I'm an avid reader! Perhaps you are too. Regardless of your reading habits, many bibliophiles enjoy keeping track of the books we read and those we'd like to read in the future. While there is no shortage of web and mobile apps for logging your love for all things literary, none of them have quite resonated with me. So, I decided to build my own!
As with any new software project, the wise thing to do getting started would be to thoughtfully define the app's intended functionality, plan the user workflow, wireframe and prototype the user interface, and architect the database. That stuff can wait. . .
I'm a bookworm! What I care about most is where to find all the book data I'll need!
One option for this app is a traditional CRUD solution, where I, as the user, would provide all of the data for a new book record through submitting a form. That's doable. But it's not very scalable. Aside from inputting individual book data, I don't want to be required to upload and store an image file for each individual book I log in my app.
Thankfully, there are APIs available for fetching book data from large-scale, legitimate databases including Goodreads, The New York Times, Internet Archive, and others.
The API I'm using here, however, is the Google Books API because, well . . . Google. They're pretty good at indexing, amirite? Plus, Google Books' mission is literally "to digitize the world's book content and make it more discoverable on the Web."
Now, before we go any further, I'm assuming that you have (or know how to generate) a new Ruby on Rails project. If doing so is new to you, no sweat. Pause this tutorial, and go check out Getting Started with Rails. I can wait . . .
With a basic Rails application ready, we are set to begin!
Creating a New Google Cloud Platform Project
In order to leverage the Google Books API, we need to create a new project on the Google Cloud Platform (don't worry, it's free). After ensuring that you are logged into your preferred Google account, navigate in your browser to https://console.cloud.google.com.
From the lefthand sidebar, select Enabled APIs & Services from the APIs & Services dropdown menu.
On the subsequent page, click the CREATE PROJECT button. This will redirect you to a new project form where you will be asked to provide a name for your project.
Assuming the new project create request succeeded, you will be redirected to a Google Cloud Platform dashboard for your new APIs & Services project. From this view, click the + ENABLE APIS AND SERVICES button.
In the API Library search box, search for "Books API". Select the Books API result (the one with the description of The Google Books API allows clients to access the Google Books repository.). Once redirected, click ENABLE.
At this point, you should find yourself on an API/Service Details view scoped to your newly created Google Cloud Platform project. Assuming I've not led you astray, there should be a CREATE CREDENTIALS button in the top-right of the user interface. Click it!
On the Create Credentials page, make sure that "Books API" is the selected API. And check the Public data radio button. Having verified the correct options, click Next.
Voila! You should now be presented with your very own API Key. This piece of information is important. Any requests we make to the Books API must be accompanied by your app's API key to correctly identify your project and grant API access.
At this point, Google Books knows who we are and has given us permission (in the form of an API key) to request data from their API. Though we now have permission, we don't yet have the ability to send those requests from our Rails app. For that, we need an HTTP client!
Faraday as Our HTTP Client
As with choosing our book data API, there are several options for an HTTP client to choose from and implement in our Ruby on Rails app. The particular option I selected for my own app and this tutorial is Faraday, which describes itself as providing "a common interface over many adapters (such as Net::HTTP) and embraces the concept of Rack middleware when processing the request/response cycle."
As with any other Ruby gem, we can add Faraday to our Rails app by executing gem install faraday
from inside our app's directory in Terminal.
Woot! We're finally to the part where we get to write some code! But what code are we going to write?
Creating a Custom lib
Class
Specifically, we are going to create our own custom class, which we'll name "GoogleApi". To do so, create a new file named google_api.rb
in the lib/
directory of your Rails app, like so:
# FILEPATH (for reference): lib > google_api.rb
class GoogleApi
end
Our GoogleApi
class is going to be composed of three different parts:
- Constant variables, for storing string values we will use to construct our requests' url and query string
- An
initialize
method, for creating a new instance of our GoogleApi class, and - A
query
method, for utilizing Faraday to send our API request and then Ruby's built-in JSON class to parse our response.
I'll show you what the file should look like first, and then I'll explain what each part is doing.
# FILEPATH (for reference): lib > google_api.rb
class GoogleApi
BASE_URL = "https://www.googleapis.com/books/v1/volumes?"
API_KEY = "key=YOUR_GOOGLE_API_KEY"
def query
request = Faraday.get(BASE_URL+@search_format+API_KEY)
JSON.parse(request.body)
end
def initialize(user_search_input)
@search_format = "q=#{user_search_input.gsub(" ", "%20")}&"
end
end
From reading the Using the API section of the Google Books APIs developer docs, we see that performing a basic search requires that we send an HTTP GET
request to the following url:
https://www.googleapis.com/books/v1/volumes?q=search+terms
In our GoogleApi
class, I've opted to store the resource path of our URL (the section beginning with https://
and ending with the final character preceding the ?
) in a constant I've named BASE_URL
.
I then store my registered app's API key in a constant I've named API_KEY
.
One note here: Though the Books API developer docs state that "The API key is safe for embedding in URLs; it doesn't need any encoding", I like to play it safe and store potentially sensitive information as environment variables in my Rails apps. You can read about using the Figaro Ruby gem for environment variables for your app here.
At this point, we need to add an initialize
method to our GoogleApi
class in order to enable creating instances of this class which can then be queried. As seen in the file above, we can pass our potential book search queries into our class instance by allowing for a user_search_input
argument. Within our def initialize
method, we then need to do some string interpolation to format our query string before we append it to our resource path (which we stored in the BASE_URL
constant).
I want to point out a few details in our string interpolation, which you might or might not be familiar with. First, we begin our string with the ?q=
characters. In the context of our URL, these characters work together to say "Hey, server! The params for this request can be found after the ?
. Treat q=
as an empty variable that our search term(s) will be assigned to."
After ?q=
we then interpolate whatever search term was passed to our initialize
method. This search term could potentially be one or more words separated by spaces. So, we use the .gsub
method to substitute any space character with the %20
character pairing for representing spaces in URLs. Lastly, we append the &
in preparation for including our API key as an additional parameter.
Having stored our query string params to the instance variable @search_format
, we can then refer to it anywhere else inside any given instance of the GoogleApi
class, including our def query
method.
It's here that Faraday
gets used. Since only GET
requests to the Books API for public data are allowed, we know that all of our API calls for this app are going to utilize Faraday's get
method. As shown above, we pass our properly formatted URL (with query string) as the argument and store the returned value to our local request
variable.
In order for us to easily work with the data returned in the response of our GET
request to the Books API, we parse it into JSON format and implicitly return our newly formatted response.
Testing Requests/Response via Rails Console
We can now start a Rails console within our project's directory in Terminal to test our ability to send requests and receive JSON formatted responses.
rails console
> load 'lib/google_api.rb'
=> true
> GoogleApi.new("To Kill a Mockingbird").query
=>
{"kind"=>"books#volumes",
"totalItems"=>2599,
"items"=>
[{"kind"=>"books#volume",
"id"=>"PGR2AwAAQBAJ",
"etag"=>"wvc/BWKJnXY",
"selfLink"=>"https://www.googleapis.com/books/v1/volumes/PGR2AwAAQBAJ",
"volumeInfo"=>
{"title"=>"To Kill a Mockingbird",
"authors"=>["Harper Lee"],
"publisher"=>"Harper Collins",
"publishedDate"=>"2014-07-08",
"description"=>
"Voted America's Best-Loved Novel in PBS's The Great American Read Harper Lee's Pulitzer Prize-winning masterwork of honor and injustice in the deep South—and the heroism of one man in the face of blind and violent hatred One of the most cherished stories of all time, To Kill a Mockingbird has been translated into more than forty languages, sold more than forty million copies worldwide, served as the basis for an enormously popular motion picture, and was voted one of the best novels of the twentieth century by librarians across the country. A gripping, heart-wrenching, and wholly remarkable tale of coming-of-age in a South poisoned by virulent prejudice, it views a world of great beauty and savage inequities through the eyes of a young girl, as her father—a crusading local lawyer—risks everything to defend a black man unjustly accused of a terrible crime.",
"industryIdentifiers"=>[{"type"=>"ISBN_13", "identifier"=>"9780062368683"}, {"type"=>"ISBN_10", "identifier"=>"0062368680"}],
...
If all we wanted to do was to play around with requests to the Books API in a Rails console, we'd be done. But we're here to build a Rails web app and make our search requests through the user interface loaded in a web browser! So, we have a few more things we need to do.
Specifically, we still need to:
- Create a controller for searching the Books API,
- Provide some sort of UI for displaying our search responses, and
- Wire up our search controller action to a search box in our web app.
Wiring Up the Controller
As for our controller, let's create a new file named searches_controller.rb
in the app > controllers
directory. Inside this file, let's create a SearchesController
class that inherits from ApplicationRecord, like so:
# FILEPATH (for reference): app > controllers > searches_controller.rb
class SearchesController < ApplicationController
require 'google_api'
def google_books_search
@response = GoogleApi.new(params[:search]).query
end
end
Within our SearchesController
class, we require our google_api
lib class in order to gain access to that class' methods.
We then define a new controller method def google_books_search
, which:
- initializes a new instance of the
GoogleApi
class, - passes the search query from the params hash as an argument, and then
- invokes the
query
instance method from ourGoogleApi
class.
Excellent!
Before we get ahead of ourselves, let's not forget to add a custom route for our newly defined controller method.
Rails.application.routes.draw do
get '/search', :to => "searches#google_books_search"
...
end
Rendering Search Results In Views
Now for rendering our search results in our app's user interface. For that, let's create a new .erb
template named books_results.erb
in a custom searches
directory inside of our app's views
directory. We can then check to see if we have a response to render. If we do, we can iterate over all response items and display whatever content we'd like. If there is no response for us to iterate over, we can conditionally render "No search results." in the UI instead.
For this app, let's display each search result as a formatted string containing the book's title and main author.
# FILEPATH (for reference): app > views > searches > books_results.erb
<% if @response %>
<% @response["items"].each do |item| %>
<% book = item["volumeInfo"]%>
<%= book["title"] %> by <%= book["authors"].first %>
<hr>
<% end %>
<% else %>
No search results.
<% end %>
Last, but not least: our search field!
For this, let's create a _nav.html.erb
partial inside the views > layouts
directory with the intention to display our search box as part of our web app's main navbar.
Within our partial file, we can utilize the Rails form_tag
helper method to wrap a text_field_tag
and a button_tag
inside a do-end block. Though on the surface, our user will see only a text field input and a button to execute our search, together they will be operating as a Rails form within our navbar.
# FILEPATH (for reference): app > views > layouts > _nav.html.erb
<%= form_tag('/search', :method => :get, :class => "nav-search") do %>
<%= text_field_tag(:search, params[:search], :class => "form-control mr-sm-2") %>
<%= button_tag("Search", :class => "btn btn-outline-secondary my-2 my-sm-0") %>
<% end %>
With our search functionality built into our nav partial, let's make sure we render it throughout our app by including it inside the <body>
tags in our app's application.html.erb
file, like so:
# FILEPATH (for reference): app > views > layouts > application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Google Book API Tutorial</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<%= render 'layouts/nav' %>
<%= yield %>
</body>
</html>
Conclusion
Like an overly cooked steak, we are well done! Together, we learned how to:
- register a new project in the Google Cloud Platform and generate an API key,
- create a custom lib class that formats and enables requests and responses,
- utilize an HTTP client like Faraday to send API requests to external servers, and
- fetch API data through user-submitted queries in a Rails user interface from a browser.
With our new skills and knowledge what should we do next? When you’re ready, head over to part 2 of this series, where I'll walk us through how to code the ability to create book and shelf records that persist to a database!