Universal React with Rails: Part II

react on railsMay 01, 2015Dotby Alex Fedoseev

Building JSON API

I wrote demo app called Isomorphic comments, which allows visitors leaving a comments (thats it, pretty useless, but whatever).

Demo app

App consists of 2 parts:

Each part has its own git repo, and they are completely independent.

Also I wrote Yeoman generator to scaffold such kind of applications with one command in terminal, but before using it, it’s better to read the whole story to understand what it’s actually doing. In the last post of the series I will get back to it with the details.

Anyway, here it is: generator-flux-on-rails

RESTful API

A few words about RESTful APIs:

  • API is an abbreviation of Application Program Interface.
  • API is not about the view layer. At all. It doesn’t have user interfaces. It doesn’t interact directly with user in any way.
  • API is not about the state. It’s a stateless system. It means that API knows nothing about the client before request and it forgets him right after request is processed and response is sent. No sessions, no cookies. One night stand programmatically.
  • API is about data. Serving, storing, processing, validating. It’s the source of truth and origin of business logic.

Interaction with RESTful API is based on:

  • Standart HTTP methods (GET, POST, DELETE etc.)
  • And hypertext links

To the point

Let’s start with project’s structure.

|-- isomorphic-comments/              # Root project dir

|   `-- isomorphic-comments-api/      # Rails api

|   `-- isomorphic-comments-app/      # Node/React app

As I’ve already said each part of the app has its own git repo.

Technologies

Tools we’re gonna use for Rails part in development process:

Note about generator

Most of the steps, described below, automated by Yeoman generator, so you can use it (as is or your fork) to scaffold such kind of apps in the future.

Building the app

Create new RVM gemset to isolate dependencies:

$ rvm gemset create isomorphic-comments

Install rails-api gem:

$ gem install rails-api

Create new Rails app:

$ mkdir isomorphic-comments && cd isomorphic-comments

$ rails-api new isomorphic-comments-api --skip-sprockets --skip-bundle --database=postgresql

Create RVM files ('.ruby-version' & '.ruby-gemset') inside Rails app dir. These files will tell RVM which version of ruby and which gemset to use for this app.

$ cd isomorphic-comments-api

$ echo $RUBY_VERSION >> .ruby-version

$ echo 'isomorphic-comments' >> .ruby-gemset

Install gems

Edit Gemfile. That’s what we need to get started:

source 'https://rubygems.org'

gem 'rails', RAILS_VERSION_HERE

gem 'rails-api'

gem 'active_model_serializers', '~> 0.8.3'

gem 'responders'

gem 'pg'

gem ‘active_model_serializers’ (Github)

This gem is used for serializing models to generate proper JSON responses. Note the version — it should be '~> 0.8.3', not '0.9.X'.

gem ‘responders’ (Github)

'respond_*' helpers was extracted out of Rails core to separate gem, so we have to add it to Gemfile.

That’s all we need for now.

$ bundle install

Setup databases

Fill in database.yml:

default: &default

  adapter: postgresql

  encoding: unicode

  pool: 5

  host: localhost

  port: 5432

development:

  <<: *default

  database: isomorphic_comments_dev

test:

  <<: *default

  database: isomorphic_comments_test

production:

  <<: *default

And create dbs:

$ bundle exec rake db:create:all

Setup controllers and routes

This is JSON API, so let’s declare it in application controller:

# app/controllers/application_controller.rb

class ApplicationController < ActionController::API
    respond_to :json
end

This way we are telling that api will be responding to JSON format. All other controllers will inherit it from this one.

Next we should isolate API controllers under namespace and add versioning. So let’s do this.

To add namespace in Rails create corresponding folders inside ''app/controllers' dir:

|-- isomorphic-comments-api/

|   `--app/

|      `-- controllers/

|          `-- api/                         # api namespace

|              `-- v1/                      # api version 1 scope

|                  # api v1 controllers

|              `-- v2/                      # api version 2 scope

|                  # api v2 controllers

Having versioning you can easily roll out new releases of api without breaking the clients apps (web / mobile / etc.).

Next we should setup routes to these controllers:

# config/routes.rb

Rails.application.routes.draw do
    namespace :api do
        scope module: :v1 do
            resources :comments
        end
    end
end

This is basic setup, and we can improve it:

  1. It’s a JSON API, so we should declare that we are expecting requests in JSON format.
  2. Let’s move our api requests under 'api' subdomain, as we planned it in the previous post.

Here it is:

# config/routes.rb

Rails.application.routes.draw do
    namespace :api, defaults: { format: :json }, constraints: { subdomain: 'api' }, path: '/' do
        scope module: :v1 do
            resources :comments
        end
    end
end

Now all controllers available on the following routes:

http://api.isomorphic-comments.com/v1/

To ensure all requests are JSON, we can add this to application_controller.rb:

# app/controller/application_controller.rb

before_action :ensure_json_request

def ensure_json_request

  return if request.format == :json

  render :nothing => true, :status => 406

end

The usage of “defaults: { format: :json }” in the API namespace does check for the JSON format, but does not impose a constraint if it is not found. Instead it continues to check the routing hierarchy. By adding the above filter, we truly ensure the request is in JSON format only.

Thanks to @Chris_Ickes for catching this!

And one more thing. There’s a good practice to match the api version through Accept HTTP Header instead url. To set this up we will use custom constraint class:

# lib/api_constraints.rb

class ApiConstraints

    def initialize(options)
        @version = options[:version]
        @default = options[:default]
    end

    def matches?(req)
        @default || req.headers['Accept'].include?("application/vnd.isomorphic-comments.v#{@version}+json")
    end

end
# config/routes.rb

require 'api_constraints'

Rails.application.routes.draw do
    namespace :api, defaults: { format: :json }, constraints: { subdomain: 'api' }, path: '/' do
        scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
            resources :comments
        end
    end
end

Now in each request to API we should include Accept header with api version:

application/vnd.isomorphic-comments.v1+json

Where vnd is Vendor MIME Type.

If no Accept header present, then request will be routed to default api version scope.

Create resource

Let’s create Comment model + controller and add some data to db.

$ rails g model Comment author:string comment:text
# app/models/comment.rb

class Comment < ActiveRecord::Base
    validates_presence_of :author, :comment
end

Run the migration:

$ bundle exec rake db:migrate

Create controller:

$ rails g controller api/v1/comments index show create
# app/controllers/api/v1/comments_controller.rb

class Api::V1::CommentsController < ApplicationController

    def index
        @comments = Comment.all.order(:id).reverse
        respond_with @comments
    end

    def show
        @comment = Comment.find(params[:id])
        respond_with @comment
    end

    def create
        @comment = Comment.new(comment_params)
        if @comment.save
            render json: @comment, status: 201
        else
            render json: { errors: @comment.errors.full_messages }, status: 422
        end
    end


    private

        def comment_params
            params.require(:comment).permit(:author, :comment)
        end
    
end

And seed some data (don’t forget to run nginx):

$ curl http://api.lvh.me/comments -X POST -H 'Content-type:
application/json' -d '{"comment":{"author": "Me", "comment": "Hello,
world!"}}'

Or you can do it within db/seeds.rb

Now we can make request and get the response with JSON array of comments (one comment actually):

$ curl http://api.lvh.me/comments -X GET -H 'Content-type: application/json'

=> {"comments":[{"author": "Me", "comment": "Hello, world!"}]}

Testing

I’m not gonna go into detail here. Just mention that I’m using RSpec for testing and few more basic points:

  • Use factory-girl factories instead fixtures to seed data for tests.
  • Use shoulda-matchers to simplify your models specs.
  • Use database_cleaner to handle database stuff during the tests.
  • Prefer requests rather than **controllers **specs.

You can take a look at my specs folder in demo app.

Remember: when you’re testing API, main things that you should check via integration tests are:

  • Response body (contents of received JSON).
  • Response http code. Here they are btw.

HTTP codes

This is important to respond with correct http codes. The list of the commonly used ones:

200 :ok

Standard response for successful HTTP requests (GET mostly).

201 :created

Respond with it when resource was successfully created (POST mostly).

204 :no_content

Successful empty response (mostly used as response on DELETE method, when resource was successfully destroyed).

401 :unauthorized

Respond with it when guest user trying to access to resource, which is available to authorized users only (who provided correct login/password).

403 :forbidden

This code should be used when authorized user trying to access to resource without explicit permissions (for example logged-in user is trying to access to admin area).

404 :not_found

Requested resource not found.

422 :unprocessable_entity

This code should be sent when there were errors during model validation (empty email field for example) or request just has incorrect JSON format.

500 :internal_server_error

BIG BADA BOOM! Something wrong with the server.

Further reading

I haven’t told anything about caching & HATEOAS, because it’s slightly out of topic, but you should definitely check these parts by yourself.

Authentication part will be covered in one of the next posts (together with client side authentication).

Also I recommend to take a look at “APIs on Rails” book by Abraham Kuri.

Conclusion

We’ve built JSON API, which can handle the data, but it’s still insecure and have no client app to interact with. In the next post we’ll create one with Node.js, Express and React.

See you next time!

Part I: Planning the application

Part II: Building JSON API

Part III: Building Universal app

Part IV: Making Universal Flux app

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at [email protected] or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right