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?