Building a Super Simple Rails API

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!

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus