More and more developers are building client-side applications to consumer JSON APIs. Rails allows us to build APIs that serves JSON to a client-side framework by using Rails::API, a lighter-weight subset of a Rails application designed specifically to serve JSON.
Rails::API used to be available only through the rails-api
gem. In April, however, it was merged into Rails and now we can build out our Rails APIs just using Rails out of the box.
In this post, we'll be using Rails::API to build a super simple JSON API.
Getting Started
Our API will serve JSON to a separate application, built with Ember, that displaying Readmes to a user and allows a user to annotate those readmes. So, on the backend, we will have a Readme
model and and Annotation
model. A readme will have many annotations and an annotation will belong to a readme.
To begin, we will generate our new Rails API with:
rails-api new notebook_api
Generating our API gets us a few things for free:
- Our Gemfile contains the
rails-api
gem. - Our Application Controller inherits from
ActionController::API
.
Let's quickly set up and run our migrations.
Migrations
Create the Readmes table:
class CreateReadmesTable < ActiveRecord::Migration
def change
create_table :readmes do |t|
t.string :title
t.string :content
end
end
end
And the Annotations table:
class CreateReadmesTable < ActiveRecord::Migration
def change
create_table :readmes do |t|
t.string :quote
t.string :text
t.json :ranges, default: {}
t.integer :readme_id
end
end
end
Models
Let's build out our models.
class Readme < ActiveRecord::Base
has_many :annotations
end
class Annotation < ActiveRecord::Base
belongs_to :readme
end
Now we're ready to move on to the fun stuff.
Serializers
In order for our Rails API to serve JSON, we need to tell our application how to serialize data as JSON. For this, we'll use the Active Model Serializers gem.
In your Gemfile, add: gem 'active_model_serializers'
and bundle install.
Then, create a folder in the top level of the app
directory called serializers
.
Create a file app/serializers/readme_serializer.rb
class ReadmeSerializer < ActiveModel::Serializer
attributes :id, :content, :title, :annotation_ids
end
Inherit the serializer from ActiveModel::Serializer
and list the attributes of a given readme that we need to be serialized as JSON. Only the attributes we list here will be exposed via our API/included in the payload sent in response to a request for a readme.
Note that we include the :annotations_ids
so that a readme the client-side application that hits our API can easily load the annotations associated to a give readme.
Let's do the same for our Annotation model. Create a file app/serializers/annotation_serializer.rb
class AnnotationSerializer < ActiveModel::Serializer
attributes :id, :text, :quote, :ranges, :readme_id
end
Routes
We want to namespace our API endpoints under api::v1
, so we'll define our routes like this:
# config/routes.rb
namespace :api do
namespace :v1 do
resources :readmes
resources :annotations
end
end
Now that our routes are set up, we can build our controllers.
Controllers
Before we build out our individual Readmes and Annotations Controllers, we need to add something to our Application Controller.
Add the following line to the Application Controller:
include ActionController::Serialization
This will ensure that the serialized data will be stored under top-level keys of readmes
or annotations
when the API receives a request for all of the readmes or all of the annotations.
Now let's build out our controllers. We'll nest our controllers in app/api/v1
to reflect the namespaced routes we defined.
Readmes Controller
# app/controllers/api/v1/readmes_controller.rb
class Api::V1::ReadmesController < ApplicationController
def index
render json: Readme.all
end
def show
render json: readme
end
def create
render json: Readme.create(readme_params)
end
def update
render json: readme.update(readme_params)
end
def destroy
render json: readme.destroy
end
private
def readme
Readme.find(params[:id])
end
def readme_params
params.require(:readme).permit(:title, :content)
end
end
Note that our controller is namespaced as Api::V1
and that each controller method returns JSON-ified data via render json:
.
I won't show the Annotations Controller here because it is super specific to the Annotator.js library that I'm using on the client-side. If you're interested though, you can check out this post.
Lastly, thought, let's set up CORS so that our client-side app can successfully request and receive data from this API.
Setting Up CORS
We'll be using the Rack-CORS gem and middleware to allow our API to support cross-origin requests.
- Add the gem to your Gemfile,
gem 'rack-cors', :require => 'rack/cors'
and bundle install. - In
config/application.rb
, set up the CORS middleware:
require File.expand_path('../boot', __FILE__)
require 'rails/all'
Bundler.require(*Rails.groups)
module LearnNotebookApi
class Application < Rails::Application
config.api_only = true
config.middleware.insert_before 0, "Rack::Cors" do
allow do
origins '*'
resource '*', :headers => :any, :methods => [:get, :post, :put, :delete, :options]
end
end
config.active_record.raise_in_transactional_callbacks = true
end
end
Conclusion
And that's it! This has been a super-simple walk-through of a super-simple Rails API. A few areas of continued development:
- Implementing API keys
- Other stuff? I'm open to suggestions!