Luis Martinez
Aug 12, 2021

Using an External API in your Ruby on Rails Application

APIs provide a link between a client and a server so that the client can get (and, depending on the level of authorization for the client), post, edit, and/or delete information from the server. An API can be considered as a database. You can create your own API or you can use someone else’s API (an external API). In this article, I would like to discuss my experience using an external API for the first time in my Ruby on Rails project, Hotel Booking, as part of Flatiron software engineering third milestone project. [Note: this is one of my Medium articles transferred to DevBlog. You can find the original article here for comparison purposes]

Setting Up your App for API Use

Hotel Booking is an app that allows users to search and make hotel reservations in real-time at more than 150,000 hotels worldwide (note: the reservations are valid in real-time at time of booking but booking takes place within the application only). This is possible due to the app using an external API, the Amadeus Hotel Search API. As discussed above, this was my first time using an external API, and I would like to provide as many details as possible on how to use an external API since it took me a while to get things to work.

The first thing is to get an API that you want to use for your application. For example, say your application includes a weather feature, where the user enters data for a given location and the application returns the current weather conditions for that location. Your ruby on rails application does not count with this weather information, so you need to set up your application to make a request to an external source to get the weather information. An example of such an external source would be the AccuWeather API. You may click on this link for a starter list of APIs.

Once you know the API you want to use, the next step is to familiarize yourself with the documentation for the API. The format to make a request to an API may vary from one API to another, but generally the request format is as follows: 

"base_url/endpoint"

The base url is a constant, while the endpoint changes depending on the information you want to get. For example, a request to the AccuWeather API for the current whether conditions at a given location looks like this

http://dataservice.accuweather.com/currentconditions/v1/{locationKey}

where "{locationKey}" is the endpoint and needs to be replaced with the key for the location you want the information from (you need to follow the API documentation to get a locationKey)

For my project I used the Amadeus Hotel Search API. The API provides real-time hotel reservations information for more than 150,000 hotels. Just for reference, along with Amadeus, Sabre and Travelport are the three major global distribution systems (GDS) worldwide (GDS are used by travel agencies for making travel reservations for customers). You may take a look at this website for more info on travel APIs.

Some APIs require that you get an API key and an API secret in order to use the API. To get those you need to follow the API’s documentation (getting the key and secret usually involves creating a developer account with the API company). The Amadeus Hotel Search does require an API key and secret.

Once you get your API key and API secret (if required by the API), create a file called “.env” in the main directory of your Rails app:


Figure 2. Location of the “.env” file within a Rails app.

Within this “.env” file create variables to store the API key and API secret (that you got from the API’s website). For example: 

AMADEUS_API_KEY = pasteYourAPIkeyHere

AMADEUS_API_SECRET = pasteYourAPIsecretHere

To be able to later use these two variables in your application, you need to include the dotenv-rails gem in your gemfile to make the variables environment variables.

From now on, I’ll show what I did to get the API to work for my app (after doing lots of research); however, be aware that there may be variations on how to implement the next steps.

The next step will be to create a class for a Plain Old Ruby Object (PORO) for your API. A PORO is just a plain ruby object, without ActiveRecord. Create a file within your models folder to hold the API class. This was the class I created for the project: 

# In app/models/amadeus_api.rb
require 'amadeus'
class AmadeusApi
  attr_accessor :amadeus  
  def initialize()
    @amadeus = Amadeus::Client.new({
      client_id: "#{ENV['AMADEUS_API_KEY']}",
      client_secret: "#{ENV['AMADEUS_API_SECRET']}"
    })
  end
  # ...more code
end

Let us break down the above code:

  • We created a plain Ruby class, AmadeusApi.
  • We required the ‘amadeus’ gem at the top of the file. Why? The documentation for the API tells us to do so (also, include the amadeus gem in your gemfile).
  • Instantiating an object from this class requires to use your API key and API secret, which are automatically loaded from the “.env” file, and you do not need to replace their corresponding values in the above code.

We also have an Amadeus::Client class (inside the initialize method). Where did it come from? We need to use an instance of this Amadeus::Client class in order to make requests to the API. The documentation, which you can find here, provides it to us.

There are a couple of things to care of here. For example, what happens when we write:

myObject = AmadeusApi.new

Well, first, we get a new instance of the PLAIN (NON SPECIAL) AmadeusApi ruby class. That PLAIN ruby instance is stored in the myObject variable. Can we make an API request just using myObject? No, it is a plain ruby class and it is not connected to the API. Now, when we write myObject = AmadeusApi.new an instantiate of the Amadeus::Client class is also created AND STORED in the @amadeus instance variable (look at the above initialize method). How can we access this instance of the SPECIAL Amadeus::Client class? We wrote an attribute accessor for the variable “amadeus”; hence, if we now write 

myObject.amadeus

we will get the Amadeus::Client object stored in the @amadeus instance variable. What is the usefulness of all this? Well, say that you are in one of your controllers and you want to make a request to the API. For example, in my hotels controller I need to check that a reservation is still available when a user clicks on “Reserve” before creating a new reservation. I make an API request to the reservation endpoint like this: 

# In hotels_controller.rb
def reserve  
  # Get an Amadeus::Client object
 api = AmadeusApi.new.amadeus 
  # Use the Amadeus::Client object to make a request to the API to see whether the offer with the given code is still available    
 reservation = api.shopping.hotel_offer(params[:code]).get.data 
  # ...more code
end

reservation above will be an array that will contain (among many other things) an ‘available’ key with a value of true or false. You will need to later manipulate the reservation variable to get the ‘available’ key.

The right hand side of ‘reservation’ in the above code is a get request to the API. You need to study your API documentation to know the available variations (the endpoints) for the API requests.

Notice that the above get request contains one argument only. Get requests can be a little more involved and that’s where our plain old ruby class comes into play. Consider the below code snippet:

def index 
  #...some code handling nested routes
 # Code for handling making an API request 
  if params[:city] && !params[:city].blank? 
    api = AmadeusApi.new 
    user_id = current_user.id 
    begin 
      @hotels = api.query_city(
        params[:city], 
        params[:checkin_date], 
        params[:checout_date], 
        params[:guests], user_id) 
    rescue StandardError => e 
      flash[:msg] = "#{e.class}: #{e.message}. Invalid city code or input value. Please try again..." 
      render :'index.html.erb' and return 
    end 
    if @hotels.empty? 
      flash[:msg] = "Ooops, no hotels could be found for the requested specifications" 
    end 
  end 
end

On line 8 we invoke an instance method (#query_city) on an instance of our plain ruby AmadeusApi class. Let us go to our AmadeusApi class to see this method:

require 'amadeus'
class AmadeusApi  
  attr_accessor :amadeus  
  def initialize() 
    @amadeus = Amadeus::Client.new({ 
      client_id: "#{ENV['AMADEUS_API_KEY']}", 
      client_secret: "#{ENV['AMADEUS_API_SECRET']}" 
    }) 
  end  

  def query_city(
    citycode, 
    checkin_date = Date.today.to_s, 
    checkout_date = (Date.today+1).to_s, 
    guests = 2, 
    user_id
   ) 
   response = @amadeus.shopping.hotel_offers.get( 
      cityCode: citycode, 
      checkInDate: checkin_date, 
      checkOutDate: checkout_date, 
      adults: guests, 
      currency: "USD" 
    ).data 
    parse_city_responnse(response, user_id) 
  end    
  # ...more code
end

We have the ‘query_city’ method on line 12. What does this method do? It makes an API request, @amadeus.shopping.hotel_offers( #request parameters and their corresponding value). What is @amadeus? It is the instance variable containing our Amadeus::Client object for making API requests. This request contains five parameters, and we can get the syntax for each parameter from the API documentation. Bottomline: familiarize yourself well with the API documentation, specially for (relatively) complex ones, such as this one, where you have plenty of options for the request body.

Error Handling for API Responses

I would like to go over error handling. When making an API request, the response is not always a json object or an array, it could be an error, and the error can make your app crash (stop working). That is why we need to handle errors when dealing with APIs. On line 7 in figure 3 we have a ‘begin” block. Begin blocks usually have the following format: 

begin
  # Some code where you get a response from an API
rescue
 # Some fallback action in case the response comes with an error
end

To better understand error handling, consider the below figure in which I make a request using an invalid city code in my app:

Figure 5. Making an API request using invalid input for the city name field; will cause an error response from the API.

The field ‘Search for” should be the name of a city, but I just wrote a random string. Before clicking on the Search button, I went on and removed the ‘begin’, ‘rescue’ and ‘end’ words from my hotels controller (refer to figure 3 above for the code). This is what happens some seconds after clicking on the “Search” button:

Figure 6. App crashes due to error response from API

The API request returns an error and the app crashes (stops working). The error originated in the query_city method within the AmadeusApi class (see figure 4). Now, I included back the ‘begin’ , ‘rescue’ and ‘end’ key words from figure 3 in my hotels controller, and this is what happens when I make a request using invalid input for the city name field:


Figure 7. Handling an API error response using a ‘begin’ block.

The app does not crash, but rather displays a flash message containing the error details.

I found this article to be very useful in explaining how to rescue exceptions (errors).

Any comments and/or suggestions are very welcomed. You can find the git repository for Hotel Booking here.