- Describe what an API is, and why we might use one.
- Use Postman to test an API.
- Describe the purpose and syntax of
respond_to
. - Make a Rails app that provides a JSON API.
In this lesson you will learn how to build a Rails API from scratch and create a back-end that serves up JSON, in addition the usual HTML views. We will be focusing almost entirely on the back-end itself. To test our API, we will be using a very useful program called Postman.
What is an API?
An "Application Program Interface." While it technically applies to all of software design, the term has come to refer to web URLs that can be accessed for raw data.
How can we go about accessing an API within our programs?
Using jQuery's AJAX method, Angular's ngResource, or a library for requests & promises like axios, for example.
What information do we need to provide in order to be able to retrieve information from an API? What about for modifying data in an API?
In order to "GET" or "DELETE" information, we need to provide a
url
,type
, (HTTP method) anddataType
(API data format). > In order to "POST" or "PUT", we also need to provide somedata
.
Example:
$.ajax({
type: 'POST',
data: {
artist: {
name: "Limp Bizkit",
nationality: "USA",
photo_url: "http://nerdist.com/wp-content/uploads/2014/12/limp_bizkit-970x545.jpg"
}
},
dataType: 'json',
url: "/artists"
}).done((response) => {
console.log(response);
}).fail((response) => {
console.log("AJAX POST failed");
})
3 minutes exercise. 2 minutes review.
We will spend some time accessing a couple 3rd party APIs. APIs vary quite a bit in their purposes and configurations. To get a sense of that, let's take a look at a few different APIs.
Form pairs and explore the API links in the below table. Record any observations that come to mind. In particular, think about what you see in the URL and the API response itself.
The raw, unformatted JSON output that comes back from an API can be formatted by this Chrome Extension, JSON Formatter
Let's make a basic HTTP request to an API. While we can do this in the browser, we're going to use Postman - a Chrome plug-in for making HTTP requests - so we can not only look at it in more detail, but also make POST
PUT
and DELETE
from the browser without building an app.
- Download Postman.
- Type in the "url" of an API call.
- Ensure the "method" is "GET".
- Press "Send".
Here's an example of a successful 200 OK
API call...
Today, we're going to use Rails to create our own API from which we can pull information. We will be using a familiar codebase, and modify it so that it can serve up data.
Let's demonstrate using Grumblr. Clone down this starter code and checkout the api-starter
branch...
$ git clone https://github.com/ga-wdi-exercises/grumblr_rails_api.git
$ cd grumblr_rails_api
$ git checkout api-starter
$ bundle install
$ rails db:drop db:create db:migrate db:seed
$ rails s
The solution to today's code is available on the
api-solution
branch
Earlier we used Postman to make an HTTP request to retrieve information from tvmaze.com, a 3rd party API. Under the hood, that API received a GET request in the exact same way that the Rails application we have built in class thus far have received GET requests.
All the requests that our Rails application can receive are listed when we run
rails routes
in the Terminal. We create RESTful routes and corresponding controller actions that respond toGET
POST
PATCH
PUT
andDELETE
requests.
Prefix Verb URI Pattern Controller#Action
grumble_comments GET /grumbles/:grumble_id/comments(.:format) comments#index
POST /grumbles/:grumble_id/comments(.:format) comments#create
new_grumble_comment GET /grumbles/:grumble_id/comments/new(.:format) comments#new
edit_grumble_comment GET /grumbles/:grumble_id/comments/:id/edit(.:format) comments#edit
grumble_comment GET /grumbles/:grumble_id/comments/:id(.:format) comments#show
PATCH /grumbles/:grumble_id/comments/:id(.:format) comments#update
PUT /grumbles/:grumble_id/comments/:id(.:format) comments#update
DELETE /grumbles/:grumble_id/comments/:id(.:format) comments#destroy
grumbles GET /grumbles(.:format) grumbles#index
POST /grumbles(.:format) grumbles#create
new_grumble GET /grumbles/new(.:format) grumbles#new
edit_grumble GET /grumbles/:id/edit(.:format) grumbles#edit
grumble GET /grumbles/:id(.:format) grumbles#show
PATCH /grumbles/:id(.:format) grumbles#update
PUT /grumbles/:id(.:format) grumbles#update
DELETE /grumbles/:id(.:format) grumbles#destroy
root GET / redirect(301, /grumbles)
There's something under the URI Pattern
column we haven't talked about much yet: .:format
- Resources can be represented by many formats. Rails defaults to
:html
. But it can easily respond with:json
,:csv
,:xml
and others. - Which format do we need our application to render in order to have a functional API?
Please follow along.
Let's set up Grumblr so that it returns JSON. Grumbles#show
is a small, well-defined step. Let's start there.
What do we want to happen?
If I ask for html, Rails renders html. If I ask for JSON, Rails renders json.
In particular, we want /grumbles/4.json
to return something like this...
{
"id": 4,
"authorName": "Adrian Maseda",
"content": "This is a grumble.",
"photo_url": "http://www.placecage.com/300/300",
"created_at": "2016-10-11T02:44:24.173Z",
"updated_at": "2016-10-11T02:44:24.173Z"
}
Why .json
? Check out rails routes
...
Prefix Verb URI Pattern Controller#Action
grumble GET /grumbles/:id(.:format) grumbles#show
See (.:format)
? That means our routes support passing a format at the end of the path using dot-notation, like a file extension.
Requesting "GET" from Postman, using http://localhost:3000/grumbles/3.json
as the URL, we see a lot of something. Not very helpful. What is that?
HTML? Let's test that url in our browser. What error do we see?
Rails is expecting a JSON formatted response. Let's fix this by adding some lines to our show action in our controller.
Rails provides an incredibly useful helper - respond_to
- that we can use in our controller to render data in a given format depending on the incoming HTTP request.
Our current code...
# grumbles_controller.rb
def show
@grumble = Grumble.find(params[:id])
end
Let's modify that so our app can serve up JSON...
# grumbles_controller.rb
def show
@grumble = Grumble.find(params[:id])
respond_to do |format|
format.html { render :show }
format.json { render json: @grumble }
end
end
If the request format is html, render the show view (show.html.erb). If the request format is JSON, render the data stored in
@grumble
as JSON.
Let's demo this in the browser and Postman.
Let's walk through the same process for Grumbles#index
.
What should we do?
def index
@grumbles = Grumble.all
respond_to do |format|
format.html { render :index }
format.json { render json: @grumbles }
end
end
Demonstrate in browser and Postman.
It's your turn to do the same for Comments. You should be working in comments_controller.rb
for this.
Solution...
# comments_controller.rb
def index
@grumble = Grumble.find(params[:grumble_id])
@comments = @grumble.comments.order(:created_at)
respond_to do |format|
format.html { render :index }
format.json { render json: @comments }
end
end
def show
@grumble = Grumble.find(params[:grumble_id])
@comment = Comment.find(params[:id])
respond_to do |format|
format.html { render :show }
format.json { render json: @comment }
end
end
It's high time we created a Grumble. What do we have to change to support this functionality?
- What HTTP request will we be sending? What route and controller action does that correspond to?
- What is the purpose of
Grumbles#new
? How will it factor into our API? - What do we have to change in
Grumbles#create
?
Here's our current code...
# grumbles_controller.rb
def create
@grumble = Grumble.new(grumble_params)
if @grumble.save!
redirect_to @grumble
else
render :new
end
end
What's different about
create
vs.index
+show
? What do we need to account for in ourrespond_to
block?
We need to update the response to respond to the format.
What do we want to happen after a successful save? How about an unsuccessful one?
If the save is successful, either redirect the user to the artist show page (HTML) or send back the new artist (JSON).
If the save fails, either send the user back to the new form (HTML) or send back an error message (JSON).
# grumbles_controller.rb
def create
@grumble = Grumble.new(grumble_params)
respond_to do |format|
if @grumble.save!
format.html { redirect_to @grumble, notice: 'Grumble was successfully created.' }
format.json { render json: @grumble, status: :created, location: @grumble }
else
format.html { render :new }
format.json { render json: @grumble.errors, status: :unprocessable_entity }
end
end
end
If we successfully save the @grumble
...
- When the requested format is "html", we redirect to the show page for the
@grumble
- When the requested format is "json", we return the
@grumble
as JSON, with an HTTP status of "201 Created"
If the save fails...
- When the requested format is "html", we render the
:new
page to show the human the error of their ways - When the requested format is "json", we return the error as JSON and inform the requesting computer that we have an
unprocessable_entity
.
How do we usually test this functionality in the browser? A form!
But for this lesson, we're going to continue using Postman. Here's how you do it...
- Enter url:
localhost:3000/grumbles.json
- Method: POST
- Under the "Headers" tab in the request section, add a
Content-Type
key with a value ofapplication/json
- Add your Grumble data to "Body".
{ "grumble": { "authorName": "Jesse", "title": "Jesse's new grumble", "content": "Check out this grumble!", "photoUrl": "http://placecage.com/400/400" } }
- Press "Submit".
Content-Type
is indicating what type of data we are sending to the server - not what we are expecting back.
The raw response from this request is an error page, rendered as html. Sometimes you just have to wade through the html. Scroll down until you get to the "body".
<h1>
ActionController::InvalidAuthenticityToken
in GrumblesController#create
</h1>
Additionally we can preview the html, and see a familiar rails error page.
Ah yes. Rails uses an Authenticity token for security. It will provide it for any request made within a form it renders. Postman is decidedly not that. Let's temporarily adjust that setting for testing purposes. When we go back to using html forms, we can set it back.
In our application_controller.rb
we must adjust the way Rails protects us by default:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
# protect_from_forgery with: :exception
# support API, see: http://stackoverflow.com/questions/9362910/rails-warning-cant-verify-csrf-token-authenticity-for-json-devise-requests
protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }
end
Success should look like this...
We should now get a 200
response code signifying a successful POST
request and we can preview the html page sent back as the response (our newly created artist's show page)
Your turn. Make sure we can create and update Comments via requests that expect JSON.
Solution...
Here's a sample new comment if you want to use it.
{
"comment": {
"authorName": "Bobby",
"content": "Wow, such comment"
}
}
# comments_controller.rb
def create
@grumble = Grumble.find(params[:grumble_id])
@comment = @grumble.comments.new(comment_params)
respond_to do |format|
if @comment.save!
format.html { redirect_to @grumble, notice: 'Comment was successfully created.' }
format.json { render json: @comment, status: :created }
else
format.html { render :new }
format.json { render json: @comment.errors, status: :unprocessable_entity }
end
end
end
def update
@grumble = Grumble.find(params[:grumble_id])
@comment = Comment.find(params[:id])
respond_to do |format|
if @comment.update!(comment_params)
format.html { redirect_to [@grumble, @comment], notice: 'Comment was successfully updated.' }
format.json { render json: @comment }
else
format.html { render :new }
format.json { render json: @comment.errors, status: :unprocessable_entity }
end
end
end
- Make it so that the JSON request to Comments#show only return
authorName
,content
,title
andphotoUrl
. Nocreated_at
orupdated_at
. - Make it so that the JSON request to Comments#show also includes the grumble.
- Make it so that the artists received from JSON requests to Grumbles#index and Grumbles#show also include their comments
- Make it so that when you delete a Grumble or Comment via Postman, you get a JSON object confirming that the Grumble or Comment has been deleted
You'll notice that when we access a Grumble or Comment using our API, we don't see any information about their associations. So what would we do if, for example, every time we retrieve a Grumble we also want to see all the comments that belong to it? We can use Rails' include
keyword to take care of that...
This hidden snippet contains the answer for some of the earlier bonuses, so only take a look once you've give them a shot...
How to use include
...
def index
@grumbles = Grumble.all
respond_to do |format|
format.html { render :index }
format.json { render json: @grumbles, include: :comments }
end
end
{
"id": 1,
"authorName": "Jesse",
"content": "It always seems impossible, until it's done.",
"title": "11 nerds wearing shoes",
"photoUrl": "https://splashbase.s3.amazonaws.com/snapwiresnaps/regular/tumblr_o3dxa7RePd1teue7jo1_1280.jpg",
"created_at": "2016-10-26T11:42:33.808Z",
"updated_at": "2016-10-26T11:42:33.808Z",
"comments": [
{
"id": 1,
"authorName": "Andy",
"content": "That photo reminds me of the time I saw an outgoing and lonely software engineer who is flatulently dueling a disgusting and bloated master of disguise.",
"grumble_id": 1,
"created_at": "2016-10-26T11:42:34.340Z",
"updated_at": "2016-10-26T11:42:34.340Z"
},
{
"id": 2,
"authorName": "Adam",
"content": "Who wrote this? It sounds like it was written by a snide, bloated, corrupt, and unethical Yeti in Nelson Mandela's jail cell in 1983.",
"grumble_id": 1,
"created_at": "2016-10-26T11:42:34.378Z",
"updated_at": "2016-10-26T11:42:34.378Z"
},
{
"id": 3,
"authorName": "Jesse",
"content": "I've responded to this in my post about a diseased mime who is rocking out on an air guitar with a blushing nerd.",
"grumble_id": 1,
"created_at": "2016-10-26T11:42:34.415Z",
"updated_at": "2016-10-26T11:42:34.415Z"
},
{
"id": 4,
"authorName": "Adam",
"content": "That photo reminds me of the time I saw a considerate Mafia don who is deceitfully voting.",
"grumble_id": 1,
"created_at": "2016-10-26T11:42:34.461Z",
"updated_at": "2016-10-26T11:42:34.461Z"
},
{
"id": 5,
"authorName": "Adam",
"content": "I feel like a more appropriate picture for this post would be a fat ghost who is carefully delivering pizza to a scrawny poker dealer.",
"grumble_id": 1,
"created_at": "2016-10-26T11:42:34.501Z",
"updated_at": "2016-10-26T11:42:34.501Z"
}
]
}
When you build APIs will Rails, chances are you might encounter some Cross-Origin errors. This is because your Rails API is not equipped to accept POST
PUT
or DELETE
requests from sources (or "origins") other than itself. The Rack CORS gem is a useful tool in tackling that problem.
What if we want to retrieve information from a 3rd party API from using Ruby? There are a few libraries that help with this, but the most popular of which is HTTParty.
No need to create a Rails app to run the below code. Just test it out in a lone app.rb file via the Terminal.
After adding it to our Gemfile. We can start using it right away,
We are going to be using weather underground's api to utilize httparty
to make requests and to parse JSON responses.
Make sure to register for an account, and to generate a free api key.
response = HTTParty.get('http://api.wunderground.com/api/<your key here>/conditions/q/CA/San_Francisco.json')
Checkout the response:
response.code
response.message
response.body
response.headers
Or better yet, you can make a PORO (Plain Old Ruby Object) class and use that.
class Forecast
# creates getter methods for temp_f, weather, city and state.
attr_reader :temp_f, :weather, :city, :state
# initialize method takes 2 arguments city and state
def initialize(city, state)
# create the url using the city and state arguments. Also utilizing ENV
# variable provided by figaro. Key value should be in 'config/application.yml'
url = "http://api.wunderground.com/api/#{ENV["wunderground_api_key"]}/conditions/q/#{state.gsub(/\s/, "_")}/#{city.gsub(/\s/, "_")}.json"
# utilizing httparty gem to make get request to the url prescribed in the
# line above and storing the response into the variable below.
response = HTTParty.get(url)
# instantiating temp_f and weather by parsing through the JSON response
@temp_f = response["current_observation"]["temp_f"]
@weather = response["current_observation"]["weather"]
# storing arguments as instance variables in the model
@city = city
@state = state
end
end
Currently our model won’t work because we haven’t defined ENV["wunderground_api_key"]. We need to make sure we update our config/application.yml
file with this information:
wunderground_api_key: your_key_info_goes_here
Assuming you have a working key, we can now hop into the rails console
and test our model out. We can see something like this if we instantiate a new forecast and pass in washington and dc as arguments:
washington = Forecast.new("washington", "dc")
washington.temp_f
washington.weather
If you'd like to learn more about APIs and POROs, Andy has a great blog post on the subject.