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.hosts
line toconfig/environments/development.rb
config.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: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 theadd_reference
line. (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 :posts
and the following line to
models/post.rb
belongs_to :author
-
Now go ahead and run the migration, and then examine the new
author_id
field in the posts table by examining thedb/schema.rb
.rake db:migrate
-
Now, let’s update our routes in
config/routes.rb
Rails.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.rb
we need:class ApplicationController < ActionController::API include Response include ExceptionHandler end
- 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 lineincludes Response
. This makes all methods in theResponse
module instance methods ofApplicationController
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 theincluded
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.
- 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
new
andedit
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
andupdate
aren’t packaged inside anauthors
object.
- There are no
-
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'})
- 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
posts
route. 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.)