Rails API – Tech Learning

This post is part of my tech learning series, where I take a few hours to evaluate a piece of technology that I’d like to learn.

Since I’ve been working a bit more with client side JavaScript frameworks like AngularJS and Knockout.js with my tech learning series, I decided it was a good idea to create a consistent API server for them.

It also gave me an opportunity to look into the Rails API project and compare it to how I’ve built API servers by hand.

Documentation

Rails API doesn’t come with a ton of documentation, just a Readme file. But since it’s just a customization of Ruby on Rails, that’s more than enough for me.

The Readme explains the purpose of the project, JSON APIs, what Rails itself does for APIs, setup instructions, and then how Rails API changes Rails.

In addition to the Rails API documentation, I also used the JSON API documentation. JSON API is a draft standard that describes a shared convention for building JSON APIs. There wasn’t very much in here that was new but it was nice to have a standardized set of request/response formats and codes.

(Sometimes it feels like every project reinvents what HTTP code should be returned and when. This is a waste, especially when the same team controls both the client and server)

Todo list application server with Rails API

With this project I wanted to build a basic server to persist todo items from API clients (e.g. JavaScript apps). The features are intentionally minimal so I can evaluate and compare how the Rails API project works.

  • JSON API
  • List all todo items
  • Add a new todo item
  • Complete a todo item
  • Delete a todo item

Later on I’ll probably add user accounts, possiblely with OAuth or something. I’ve done this enough that I skipped it in the interest of time.

Implementation

Overall Rails API works exactly how it says. After installing the gem you use its generator which creates a new Rails application with the Rails API customizations. I used a few options to skip the JavaScript stack completely: --skip-sprockets --skip-javascript --skip-turbolinks.

I also used active_model_serializers to standardize on how model data should be serialized into JSON. In this simple application, the generated serializer for the Todo model was good enough.

I’m only going to cover the major points in the implementation, instead of each file.

Todo model

class Todo < ActiveRecord::Base
  validates :title, :presence => true
 
  enum status: [:open, :complete]
 
end

The todo model is the class to persist todo items from clients. It has a title to hold the todo content and a Rails enum for the status (open, complete).

The nice thing about the enum is that this lets clients send the status as a string (e.g. “complete”) instead of matching the integer values. This will separate the server’s implementation from the API so it can evolve separately (e.g. change from integer to string, move to different class).

Todo controller

class TodosController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found
 
  def index
    render json: Todo.order(created_at: :asc).all
  end
 
  def show
    render json: Todo.find(params[:id])
  end
 
  def create
    @todo = Todo.new(todo_params)
 
    if @todo.save
      render json: @todo, status: :created
    else
      render json: @todo.errors, status: :bad_request
    end
  end
 
  def update
    @todo = Todo.find(params[:id])
 
    if @todo.update_attributes(todo_params)
      render json: @todo
    else
      render json: @todo.errors, status: :bad_request
    end
  end
 
  def destroy
    @todo = Todo.find(params[:id])
    @todo.destroy
 
    head :no_content
 
  end
 
  private
 
  def record_not_found
    head :not_found
  end
 
  def todo_params
    params.require(:todo).permit(:title, :status)
  end
end

The todo controller is the public API for the server.

It’s a basic Rails resource but since there are no HTML formats the new and edit actions are skipped. In fact, when generating the resource the Rails API gem excluded them in the routes.rb automatically.

To make the API clear, I used rescue_from to handle 404 requests with head :not_found. This means the server won’t return any JSON and the clients would just check the status code. This is cleaner than returning empty records or error messages in each action.

Other than that there isn’t much here that isn’t standard Rails. There’s strong_parameters in use and JSON formats are required/assumed for every request.

Todo API Test

require 'test_helper'
 
# Tests the public api for todos
class TodoApiTest < ActionDispatch::IntegrationTest
  fixtures :all
 
  # GET /todos
  test "GET /todos with no items" do
    Todo.destroy_all
 
    get "/todos"
    assert_response :success
 
    todos = JSON.parse(response.body)
    assert_equal 0, todos.length
  end
 
  test "GET /todos with items" do
    get "/todos"
    assert_response :success
 
    todos = JSON.parse(response.body)
    assert_equal 2, todos.length
    assert_equal "Walk the dog", todos.first["title"]
    assert_equal "Take out the trash", todos.second["title"]
  end
 
  # GET /todos/n.json
  test "GET /todos/n.json" do
    @todo = Todo.last
    get "/todos/#{@todo.id}.json"
    assert_response :success
 
    todo = JSON.parse(response.body)
    assert todo.present?
    assert_equal "Walk the dog", todo["title"]
    assert todo.has_key?("id")
    assert todo.has_key?("title")
    assert todo.has_key?("status")
  end
 
  test "GET /todos/n.json for an invalid item" do
    Todo.destroy_all
 
    get "/todos/10.json"
    assert_response :not_found
 
    refute response.body.present?
  end
 
  # POST /todos
  test "POST /todos.json" do
    post "/todos.json", todo: { title: "Something new" }
    assert_response :created
 
    todo = JSON.parse(response.body)
    assert todo.present?
    assert_equal "Something new", todo["title"]
    assert todo.has_key?("id")
    assert todo.has_key?("title")
    assert todo.has_key?("status")
  end
 
  test "POST /todos.json with an invalid item" do
    post "/todos.json", todo: { title: "" }
    assert_response :bad_request
 
    errors = JSON.parse(response.body)
    assert errors.present?
    assert_equal ["can't be blank"], errors["title"]
  end
 
  # PUT /todos/n.json
  test "PUT /todos/n.json" do
    @todo = Todo.last
    put "/todos/#{@todo.id}.json", todo: { title: "An edit" }
    assert_response :success
 
    todo = JSON.parse(response.body)
    assert todo.present?
    assert_equal "An edit", todo["title"]
    assert todo.has_key?("id")
    assert todo.has_key?("title")
    assert todo.has_key?("status")
  end
 
  test "PUT /todos/n.json to complete an item" do
    @todo = Todo.last
    assert_equal "open", @todo.status
 
    put "/todos/#{@todo.id}.json", todo: { status: "complete" }
    assert_response :success
 
    @todo.reload
    assert_equal "complete", @todo.status
  end
 
  test "PUT /todos/n.json with an invalid item" do
    @todo = Todo.last
    put "/todos/#{@todo.id}.json", todo: { title: "" }
    assert_response :bad_request
 
    errors = JSON.parse(response.body)
    assert errors.present?
    assert_equal ["can't be blank"], errors["title"]
  end
 
  # DELETE /todos/n.json
  test "DELETE /todos/n.json" do
    @todo = Todo.last
    delete "/todos/#{@todo.id}.json"
    assert_response :no_content
 
    refute response.body.present?
  end
 
  test "DELETE /todos/n.json for an invalid item" do
    Todo.destroy_all
 
    delete "/todos/10.json"
    assert_response :not_found
 
    refute response.body.present?
  end
end

The majority of the code I wrote is for an integration test for the entire API.

Anytime you’re publishing a public API, write integration tests for it from the perspective of your consumers.

In the integration test I’m doing standard assertions that you’d see on any Rails application, except for one difference. In every test I’m parsing the JSON returned to validate that it matches the format I expect. This is slightly verbose, especially with has_key? checks but this makes sure that the JSON returned is what I expected. This also makes it really easy to publish an API document for clients to reference.

Summary

As I expected, Rails API worked just how it’s described. It wraps Rails and removes parts an API server doesn’t need.

What was surprising to me was that Rails API doesn’t hardcode which Rails version it uses. I expected the project to have to review and upgrade it’s code with every Rails release but I was pleasantly surprised that it just depends on Rails. This meant my application brought in Rails 4.2.0 (latest version as of this article) but it would work equally well with older versions of Rails.

Rails API is also pretty flexible. You can start with an API-only application and later on convert it to a standard Rails application if you need to add views. Or you can go the other way by porting your full stack application to an API application without having to completely start fresh.

With the proliferation of client side JavaScript frameworks and single page apps, I can see Rails API becoming a regular tool for Rails developers. Instead of having to use a different and possibly new (i.e. less-stable, less-documented) server-side tool, Rails itself can be used.

Review and download the entire application

Work with me

Are you considering hiring me to help with your Shopify store or custom Shopify app?

Apply to become a client here