Skip to content

Instantly share code, notes, and snippets.

@tgaff
Created May 6, 2016 20:54
Show Gist options
  • Save tgaff/b79a23af2307539b668fdd3c7ddd0af8 to your computer and use it in GitHub Desktop.
Save tgaff/b79a23af2307539b668fdd3c7ddd0af8 to your computer and use it in GitHub Desktop.

#Building a Rails API

Objectives

After this lesson, students will be able to:

  • Create RESTful JSON endpoints for API requests using Rails
  • Add namespaced controllers for serving a JSON API
  • Add namespaced routes for a JSON API

Preparation

Before this lesson, students should already be able to:

  • Build models, routes, and controllers in Rails
  • Seed a database with a seed.rb file

RESTful API in Rails - Intro (5 mins)

Simply put: “API is program which let the user to use methods of your application from outside the application“. In this lesson, we will create a RESTful API implementing CRUD functionality on our collection of bowties - or our bowtie table in our database - from outside the web application.

Normally, you'll have forms for new and edit functionality providing a UI for users to add and edit bowties. But for an API you do not need any view as a user do not interact with your application directly; instead you'll specify the data the third parties should include as parameters in their request.

When users hit our API, we will not redirect in our controller actions; instead we'll return some data with an appropriate status code and a success or failure message. As per convention, you should return data in a format requested by the client calling your API, so for JSON request, return JSON data; for XML request, return XML data.

##Setting up our application - Codealong (20 mins)

Let's get our new app started:

$ rails new bowties-api -d postgresql -T

If we spin up our Rails server and try to visit localhost:3000, we get an ActiveRecord::NoDatabaseError message. This is one of the differences between working with SQLite vs. PostgreSQL. This error is super easy to fix:

$ rake db:create

Last thing: for some reason, Rails is trying to singularize "bowtie" as "bowty", so we need to fix that in our inflections.rb file, which lives in the config directory. While we're in there, we can also add a custom inflection for "API":

Note: Explain what inflection means in this context

  ActiveSupport::Inflector.inflections(:en) do |inflect|
    inflect.irregular 'bowtie', 'bowties'
    inflect.acronym 'API'
  end

Building our model

Let's get started by generating our model:

$ rails g model Bowtie material pattern style image_url wholesale_price:float retail_price:float

Before we forget, let's go ahead and run that migration:

$ rake db:migrate

Our Routes and Controller Actions

First, let's make a list of all our bowties! Here's what we want: we want to be able to send an HTTP GET request to /api/bowties and get back a JSON object that contains an array of all the bowties in our database. Let's try to visit that location from our browser and see what happens. No big surprise, we get a routing error that tells us we don't have a route defined.

We can fix that! Because we're building an API, we're going to do write our routes a little bit differently than we has in the past. Let's start by using Rails' resources method to generate our routes:

resources :bowties
end

First of all, let's check out what that resources method did:

rake routes
Prefix Verb   URI Pattern                 Controller#Action
    bowties GET    /bowties(.:format)          bowties#index
            POST   /bowties(.:format)          bowties#create
 new_bowtie GET    /bowties/new(.:format)      bowties#new
edit_bowtie GET    /bowties/:id/edit(.:format) bowties#edit
     bowtie GET    /bowties/:id(.:format)      bowties#show
            PATCH  /bowties/:id(.:format)      bowties#update
            PUT    /bowties/:id(.:format)      bowties#update
            DELETE /bowties/:id(.:format)      bowties#destroy

It automatically built all the routes for CRUD functionality. That's a neat parlor trick, but not exactly what we're looking for. Let's start by getting rid of all those extra routes that we don't need right now:

resources :bowties, only: [:index]

But remember, we wanted our path to be /api/bowties. Rails has a fix for that:

namespace :api do
  resources :bowties, only: [:index]
end

Within the routes routes, can add an 'api namespace'; in this way, our API functionality will not conflict with the bowties controller of our actual application. If we didn't have bowties controllers, we could simply use:

resources :bowties, :defaults => { :format => ‘json’ }

By the way, we could set the default :format, like shown here, but if you do not set it, Rails will consider it as a html request if user forgets to pass any format.

Anyway, let's assume we use the namespace: the next error we will get is ActionController::RoutingError: uninitialized constant API because we haven't defined a "thing" called API anywhere - so far, we're just referring to it in our routes.rb file. We can fix this error by doing the following:

  • create a directory called "api" inside the controllers directory.

  • inside our "api" directory, create a new file called "bowties_controller.rb".

  • Now we need to write our controller. It should look like this:

    module API
      class BowtiesController < ApplicationController
      end
    end
  • "Whoa! What is a module?", you ask? A module is just a collection of methods and/or constants (and Ruby classes--you know, like Rails controllers--are constants, so there!).

Now, if we try to visit localhost:3000/api/bowties, we no longer get that UninitializedConstant API error message. Instead, it tells us that the index action could not be found for our API::BowtiesController.

You know how to fix that! Take 30 seconds to make that error go away.

module API
  class BowtiesController < ApplicationController
    def index
    end
  end
end

The next error we should get is something about a missing template. Remember that a Rails controller's default behavior is to render an HTML view template. But that's not what we want in this case. When someone sends a GET request to /api/bowties, we want to respond with JSON so let's make that happen by adding the following to our controller:

module API
  class BowtiesController < ApplicationController

    def index
      render json: Bowtie.all
    end
  end
end

Boom! Now if we visit /api/bowties, we get an empty array. Maybe now's a good time to seed our database with some bowties so that we can actually see some data coming back. Let's do this thing:

Here's some data for our seeds.rb file:

Bowtie.destroy_all

bowties = Bowtie.create([
  {material: "silk",
    pattern: "houndstooth",
    style: "slim",
    wholesale_price: "14.98",
    retail_price: "24.95",
    image_url: "http://www.thetiebar.com/database/products/BS178_l.jpg"
  },
  {material: "silk",
    pattern: "floral",
    style: "slim",
    wholesale_price: "14.45",
    retail_price: "23.95",
    image_url: "http://www.thetiebar.com/database/products/BS184_l.jpg"
  },
  {material: "silk",
    pattern: "paisley",
    style: "traditional",
    wholesale_price: "15.65",
    retail_price: "26.95",
    image_url: "http://www.thetiebar.com/database/products/B1735_l.jpg"
  },
  {material: "wool",
    pattern: "plaid",
    style: "diamond tip",
    wholesale_price: "16.48",
    retail_price: "29.95",
    image_url: "http://www.thetiebar.com/database/products/BD325_l.jpg"
  },
  {material: "cotton",
    pattern: "gingham",
    style: "traditional",
    wholesale_price: "14.35",
    retail_price: "22.95",
    image_url: "http://www.thetiebar.com/database/products/BC570_l.jpg"
  },
  {material: "wool",
    pattern: "plaid",
    style: "traditional",
    wholesale_price: "16.48",
    retail_price: "29.95",
    image_url: "http://www.thetiebar.com/database/products/BW147_l.jpg"
  },
  {material: "cotton",
    pattern: "plaid",
    style: "slim",
    wholesale_price: "14.45",
    retail_price: "22.95",
    image_url: "http://www.thetiebar.com/database/products/BS202_l.jpg"
  },
  {material: "cotton",
    pattern: "striped",
    style: "diamond tip",
    wholesale_price: "14.48",
    retail_price: "23.95",
    image_url: "http://www.thetiebar.com/database/products/BD335_l.jpg"
  },
  {material: "silk",
    pattern: "geometric",
    style: "slim",
    wholesale_price: "15.95",
    retail_price: "28.95",
    image_url: "http://www.thetiebar.com/database/products/BT122_l.png"
  },
  {material: "silk",
    pattern: "plaid",
    style: "diamond tip",
    wholesale_price: "18.95",
    retail_price: "34.95",
    image_url: "http://www.thetiebar.com/database/products/BD324_l.jpg"
  }
])
$ rake db:seed

Now if we hit up /api/bowties we can see some delicious data.

API for show action - Independent Practice (10 mins)

Use what you just learned about creating an API for the index action and modify the routes and controller accordingly to return a sweet bowtie when you visit api/bowties/1:

{material: "silk",
  pattern: "houndstooth",
  style: "slim",
  wholesale_price: "14.98",
  retail_price: "24.95",
  image_url: "http://www.thetiebar.com/database/products/BS178_l.jpg"
}

Keep trying the api/bowties/1 endpoint and work through the errors.

Review Answers to create API for show action - Codealong (5 mins)

####Getting JSON for just one bowtie

Let's tackle getting our API to return a single bowtie. What happened when you went to /api/bowties/1?

A No route matches... error...no surprise there!

namespace :api do
  resources :bowties, only: [:index, :show]
end

The next error should have been that we didn't have a show action in our bowties controller. We can fix that!

#in our bowties_controller.rb file
def show
end

Now you should have had an error saying that the template is missing. Starting to see a pattern here? We get that error because we haven't told that controller action what to return and Rails isn't able to infer it. Let's tell it what to do:

def show
  render json: Bowtie.find(params[:id])
end

When we visit /api/bowties/1 we see some stunningly attractive JSON. Boom!

Creating and Updated Bowties - Codealong (20 mins)

Let's get serious and tackle creating a new bowtie. First thing we need to address is routing. What route do we need? Take 30 seconds and add the appropriate route to your routes.rb file.

namespace :api do
  resources :bowties, only: [:index, :show, :create]
end

Next thing we're going to need? A controller action, of course!

def create
end

Okay, this is going to look pretty similar to the create actions we have written in the past, but there are a few differences.

def create
  bowtie = Bowtie.new(bowtie_params)

  if bowtie.save
    render json: bowtie, status: 201, location: [:api, bowtie]
  end
end

private
def bowtie_params
  params.require(:bowtie).permit(:material, :pattern, :style, :image_url, :wholesale_price, :retail_price)
end

Note: Explain line 323 in detail and compare 20X status codes.

Now, let's try to create a new bowtie by sending a POST request to our API. We'll use a tool called Insomnia REST Client to do this.

To create a new POST request using Insomnia, we'll need to do a few things:

  • select the POST method from the dropdown menu

  • make sure we are posting to http://localhost:3000/api/bowties (we can figure out the route to post to by running rake routes)

  • we need to give it a payload. Make sure to select the type as JSON and then give it a payload like this:

    {"bowtie": {
        "material": "cotton",
        "pattern": "striped",
        "style": "traditional",
        "wholesale_price": "18.49",
        "retail_price": "28.99"
        }
    }

But Rails isn't going to just let you post data from some other domain. We can see that when we try to submit this POST request, we get back a 422 status code (Unprocessable Entity). This happens because Rails checks the incoming request for an authenticity token if the request is a POST, PUT, PATCH, or DELETE.

This is set up as the default for all our controllers in our Application Controller. It's that line that says protect_from_forgery with: :exception.

To fix this, we need to add the following line of code in our bowties controller to override this default behavior: protect_from_forgery with: :null_session.

Now, if we try to submit that POST request again, we will see that the POST request is successful and that it returns our newly created bowtie as JSON.

Let's tell our create action what to do if a record doesn't save because of a failed validation.

Let's start by adding a simple validation to our model so that we can see what happens if a record doesn't save:

```ruby
class Bowtie < ActiveRecord::Base
  validates :pattern, presence: true
end
```

Now, in our controller we will add an else block to our create action:

```ruby
def create
  bowtie = Bowtie.new(bowtie_params)

  if bowtie.save
    render json: bowtie, status: 201, location: [:api, bowtie]
  else
    render json: bowtie.errors, status: 422
  end
end
```

Let's try our POST request again and see what happens. We can see that the response includes a 422 Unprocessable Entity status code and it returns the error messages as JSON (in this case, we get {"pattern":["can't be blank"]}). Nice!

Updating a bowtie

Okay, no surprise here. We're gonna need a route. Quick: go write it!

```ruby
namespace :api do
  resources :bowties, only: [:index, :show, :create, :update]
end
```

Next up: no controller action! Let's define an update action in our bowties controller:

```ruby
def update
  bowtie = Bowtie.find(params[:id])
  if bowtie.update(bowtie_params)
    head 204
  end
end
```

Notice that we aren't returning the newly updated bowtie object in the response body. We're only sending back a response header with a 204 status code, which is used for a successful request that doesn't have a response body.

But what if an update fails for some reason, like not meeting a validation? We need to add an else block in our update action:

```ruby
def update
  bowtie = Bowtie.find(params[:id])
  if bowtie.update_attributes(bowtie_params)
    head 204
  else
    render json: bowtie.errors, status: 422
  end
end
```

So let's use Insomnia to send a PATCH request that tries to set an existing bowtie's pattern to null. This should fail because of our validation...and indeed it does! We get back a 422 along with the error message.

Destroying bowties - Independent Practice (10 mins)

Use what you just learned about creating an API for the other controller action and modify the routes and controller accordingly to delete a bowtie and render a 204 message in the head when use Insomnia to send a DELETE request.

Review Destroying Bowties Solution - Codealong (5 mins)

As always, we'll start with a route:

namespace :api do
  resources :bowties, only: [:index, :show, :create, :update, :destroy]
end

Next up: no controller action for "destroy". Let's write it:

def destroy
  bowtie = Bowtie.find(params[:id])
  bowtie.destroy
  head 204
end

Congratulations, you just built your first full CRUD Rails API!

Customizing our JSON response - Codealong (10 mins)

Currently, our API is returning ALL the data from our bowtie objects to the client. But maybe we want to limit the data that gets returned. For example, maybe we don't want to include the wholesale price or the datetime stamps in our JSON object. (Another good example of when you wouldn't want to return all the data is on a user model...in that case you likely wouldn't want to send clients the email, address, password digest, etc. That is private information!)

We can modify the JSON that we send by extending Rails' as_json method in our bowtie model.

```ruby
class Bowtie < ActiveRecord::Base
  validates :pattern, presence: true

  def as_json(options={})
    super(:except => [:wholesale_price, :created_at, :updated_at])
  end
end
```

Awesome! Now if we hit our /api/bowties endpoint we can see that we are only getting back the attributes that we specified when we extended the as_json method.

Let's see how you can also include the data returned from instance methods in your JSON object. We'll start by writing a method that gives us a very basic description for each bowtie, based on a few of its attributes:

def description
  "This #{pattern} bowtie is made from top quality #{material}."
end

Now if we call .description on an instance of the Bowtie class, we should get a string that looks something like "This plaid bowtie is made from top quality cotton."

To have the return value of our description method included in our JSON, we just need to modify the as_json method a little bit further:

```ruby
def as_json(options={})
  super(except:  [:wholesale_price, :created_at, :updated_at],
        methods: [:description])
end
```

Conclusion (5 mins)

  • Describe the steps involved in modifying routes and controllers to build an API for a given resource.
  • What does using namespace allow you do?
  • Why are status codes important in API req/resp?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment