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

Would you like a daily tip about Shopify?

Each tip includes a way to improve your store: customer analysis, analytics, traffic, SEO, customer acquisition, Rich Results, CRO... plus plenty of puns and amazing alliterations.