Universal React with Rails: Part II
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:
- RVM as Ruby version manager
- PostgreSQL as a database
- Rspec for tests
- rails-api gem to cut all unnecessary stuff out of Rails
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:
- It’s a JSON API, so we should declare that we are expecting requests in JSON format.
- 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