Rails Demo 5: Creating a REST API with Rails

Rails 5 introduces the ability to create API-only applications. This results in a lighter-weight app that is more conducive to creating SPAs.

Let’s explore this capability by creating a new API-only version of the Rails app we’ve built thus far.

  1. First, let’s create an API only version of our blog app.

     rails new blog-api --api
    
  2. Add the config.hosts line to config/environments/development.rb

    config.hosts << "your-app-name.codeanyapp.com"
    
  3. Next, we generate the models for posts

     rails g model Post title:string article:text likes:integer status:integer
    
  4. Next, we generate the model for authors

    rails g model Author fname:string lname:string email:string thumbnail:string
    
  5. Run the migration:

     rake db:migrate
    
  6. Now its time to formally associate authors with posts. First we need to create a new migration to update our database schema:

     rails g migration AddAuthorToPost author:references
    

    Sqlite3 has a “bug” that will complain if you try to run the migration as is. Edit the generated migration file and remove null: false from the add_reference line. (You can add this constraint later, if you like.)

  7. Now we need to inform our models of this relationship. We do this by adding the following line to models/author.rb:

     has_many :posts
    

    and the following line to models/post.rb

     belongs_to :author
    
  8. Now go ahead and run the migration, and then examine the new author_id field in the posts table by examining the db/schema.rb.

     rake db:migrate
    
  9. Now, let’s update our routes in config/routes.rb

     Rails.application.routes.draw do
       resources :authors do
         resources :posts
       end
     end
    
  10. Generate your controllers:

     rails g controller Authors
     rails g controller Posts
    
  11. Before implementing the controllers we need to define some helpers. First in app/controllers/concerns/response.rb:

     module Response
       def json_response(object, status = :ok)
         render json: object, status: status
       end
     end
    
  12. Then in app/controllers/concerns/exception_handler.rb:

    module ExceptionHandler
      # provides the more graceful `included` method
      extend ActiveSupport::Concern
        
      included do
        rescue_from ActiveRecord::RecordNotFound do |e|
          json_response({ message: e.message }, :not_found)
        end
        
        rescue_from ActiveRecord::RecordInvalid do |e|
          json_response({ message: e.message }, :unprocessable_entity)
        end
      end
    end        
    
  13. And to hook these up in controllers/application_controller.rb we need:

    class ApplicationController < ActionController::API
      include Response
      include ExceptionHandler
    end
    
  14. A Ruby module is just one way of “mixing in” code into many classes.
    • Notice that the authors and posts controllers both inherit from ApplicationController.
      • ApplicationController then has the line includes Response. This makes all methods in the Response module instance methods of ApplicationController as well as any classes that inherit from it.
      • (I’m not sure why Prof. Engelsma chose to use a module instead of simply adding the method to ApplicationController)
    • The ExceptionHandler model does something a little different:
    • When it is included in ApplicationController, the code in the included block is run inside each class.
    • This allows us to set up exception handing for each controller.
    • These exception handlers generate the appropriate json response in case the user’s request can’t be honored.
  15. Now we can implement the author controller like this:

    class AuthorsController < ApplicationController
         
      before_action :set_author, only: [:show, :update, :destroy]
         
      # GET /authors
      def index
        @authors = Author.all
        json_response(@authors)
      end
         
      # POST /authors
      def create
        @author = Author.create!(author_params)
        json_response(@author, :created)
      end
         
      # GET /authors/:id
      def show
        json_response(@author)
      end
         
      # PUT /authors/:id
      def update
        @author.update(author_params)
        head :no_content
      end
         
      # DELETE /authors/:id
      def destroy
        @author.destroy
        head :no_content
      end
         
      private
         
      def author_params
        # whitelist params
        params.permit(:fname, :lname, :email, :thumbnail, :created_by)
      end
         
      def set_author
        @author = Author.find(params[:id])
      end
    end 
    
  16. And we can implement the posts controller like this:

     class PostsController < ApplicationController
       before_action :set_author
       before_action :set_author_post, only: [:show, :update, :destroy]
         
       # GET /authors/:author_id/posts
       def index
         json_response(@author.posts)
       end
         
       # GET /authors/:author_id/posts/:id
       def show
         json_response(@post)
       end
         
       # POST /authors/:author_id/posts
       def create
         @author.posts.create!(post_params)
         json_response(@author, :created)
       end
         
       # PUT /authors/:author_id/posts/:id
       def update
         @post.update(post_params)
         head :no_content
       end
         
       # DELETE /authors/:author_id/posts/:id
       def destroy
         @post.destroy
         head :no_content
       end
         
       private
         
       def post_params
         params.permit(:title, :article, :likes, :status)
       end
         
       def set_author
         @author = Author.find(params[:author_id])
       end
         
       def set_author_post
         @post = @author.posts.find_by!(id: params[:id]) if @author
       end
     end
    
  17. A few things to notice:
    • There are no new and edit routes: An API doesn’t use forms, so we don’t need a route to display a form.
    • The views folder is empty.
    • The parameters for create and update aren’t packaged inside an authors object.
  18. Run rails console and add some authors to the DB.

    Author.create({fname: 'Mark', lname: 'Twain', email: 'mark@twain.com', thumbnail: 'test.jpg'})    Author.create({fname: 'William', lname: 'Shakespeare', email: 'will@bard.com', thumbnail: 'bard.jpg'})
    
  19. Now lets try some API calls commands:

CORS

Generally speaking, conventional browsers implement a same-origin security policy when it comes to AJAX fetches. That is, AJAX fetches to domains different from the page from which they are made are blocked by default.

Cross-origin resource sharing (CORS) is a standard that allows a web browser and server to interact in a way that permits cross-origin AJAX calls from a browser to a server. In order to use our Rails API-only app, we need to enable CORS on it. To do this, you need to include the following in the Gemfile of your rails app:

gem 'rack-cors', :require => 'rack/cors 

Don’t forget to run the bundle install command after updating the Gemfile:

bundle install

Finally, create a new file, config/initializers/cors.rb with the following content:

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:3000'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

Note that this configuration expects your React app to be running on port 3000. If you are using a different port, adjust the line above accordingly. (By default, Rails runs on port 3000 also; so, if you are running both Rails and React locally, you will have to pick a different port for one of them.)