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.
-
First, let’s create an API only version of our blog app.
rails new blog-api --api -
Add the
config.hostsline toconfig/environments/development.rbconfig.hosts << "your-app-name.codeanyapp.com" -
Next, we generate the models for posts
rails g model Post title:string article:text likes:integer status:integer -
Next, we generate the model for authors
rails g model Author fname:string lname:string email:string thumbnail:string -
Run the migration:
rake db:migrate -
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:referencesSqlite3 has a “bug” that will complain if you try to run the migration as is. Edit the generated migration file and remove
null: falsefrom theadd_referenceline. (You can add this constraint later, if you like.) -
Now we need to inform our models of this relationship. We do this by adding the following line to
models/author.rb:has_many :postsand the following line to
models/post.rbbelongs_to :author -
Now go ahead and run the migration, and then examine the new
author_idfield in the posts table by examining thedb/schema.rb.rake db:migrate -
Now, let’s update our routes in
config/routes.rbRails.application.routes.draw do resources :authors do resources :posts end end -
Generate your controllers:
rails g controller Authors rails g controller Posts -
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 -
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 -
And to hook these up in
controllers/application_controller.rbwe need:class ApplicationController < ActionController::API include Response include ExceptionHandler end - A Ruby
moduleis just one way of “mixing in” code into many classes.- Notice that the authors and posts controllers both inherit from
ApplicationController.ApplicationControllerthen has the lineincludes Response. This makes all methods in theResponsemodule instance methods ofApplicationControlleras 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
ExceptionHandlermodel does something a little different: - When it is included in
ApplicationController, the code in theincludedblock 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.
- Notice that the authors and posts controllers both inherit from
-
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 -
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 - A few things to notice:
- There are no
newandeditroutes: An API doesn’t use forms, so we don’t need a route to display a form. - The
viewsfolder is empty. - The parameters for
createandupdatearen’t packaged inside anauthorsobject.
- There are no
-
Run
rails consoleand 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'}) - Now lets try some API calls commands:
- Visit
http://yourhost.com/authors. Notice the response is JSON. - Run
curl http://yourhost.com/authors. You also get the author data as a JSON string. - Run
curl -d 'fname=George&lname=Orwell&email=george@orwell.com&thumbnail=george.jpg' https:/railsapi-kurmasz.codeanyapp.com/authors - Run
curl -d 'title=1984&article=beware&likes=434&status=published' https://railsapi-kurmaszcodeanyapp.com/authors/2/posts- Notice posts are referenced through the article.
- When creating the post, we didn’t have to pass an author id in the parameters: It came from the route itself.
- There is no
postsroute. You can only access posts of a specific author:/authors/2/posts - Run
curl http://yourhost.com/authors/2/posts
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.)