resources :zombies, only: [:index, :show]
resources :humans, except: [:destroy, :edit, :update]
Keeping an API under its own subdomain allows load balancing traffic at the DNS level.
resources :zombies, constraints: { subdomain: 'api' }
resources :humans, constraints: { subdomain: 'api' }
or
constraints subdomain: 'api' do
resources :zombies
resources :humans
end
# http://api.zombies.com/zombies
# http://api.zombies.com/humans
# /etc/hosts
127.0.0.1 zombies-dev.com
127.0.0.1 api.zombies-dev.com
``
Makes these URLs available on local machine (port still necessary)
- http://zombies-dev.com:3000
- http://api.zombies-dev.com:3000
Namespaces are useful for organizing your controller code, especially when your app has a frontend and API.
A namespace creates a subdirectory under app/controllers/
and a module that any controllers under that namespace need to go in.
# config/routes.rb
namespace :api do
resources :zombies
end
# app/controllers/api/zombies_controller.rb
module Api
class ZombiesController < ApplicationController
end
end
If you prefer to use all caps (API vs Api), then
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'API'
end
# app/controllers/api/zombies_controller.rb
module API # ALL CAPS!
class ZombiesController < ApplicationController
end
end
You can also control what matches to what. This allows you to use a subdirectory like "/api" for organizing your code without the need for an Api module.
match '/api/zombies', to: 'api/zombies#index', via: :all
match '/api/zombies/some_action', to: 'api/zombies#some_action', via: :all
match '/webhooks/mailgun/emails', to: 'webhooks/mailgun#emails', via: :post
Namespaces can be used with subdomains:
# config/routes.rb
constraints subdomain: 'api' do
namespace :api do
resources :zombies
end
end
# => http://api.zombies.com/api/zombies
# Can remove the duplication in the namespace:
constraints subdomain: 'api' do
namespace :api, path: '/' do
resources :zombies
end
end
# => http://api.zombies.com/zombies
# Can shorten it and add defaults:
namespace :api, path: nil, constraints: { subdomain: 'api' }, defaults: { format: 'json' } do
resources :zombies
end
Important characteristics:
- Safe - it should not take any action other than retrieval.
- Idempotent - sequential GET requests to the same URI should not generate side-effects.
module API
class ZombiesController < ApplicationController
def index
zombies = Zombie.all
render json: zombies
end
end
end
The to_json
method serializes all properties to JSON.
zombies.to_json == {"id":5,"name":"Joanna","age":null,"created_at":"2014-01-17T18:40:40.195Z","updated_at":"2014-01-17T18:40:40.195Z","weapon":"axe"}
curl is a great tool for sending requests to your API.
# Simple GET request
curl http://api.cs-zombies-dev.com:3000/zombies
# POST request with JSON body
curl -is -H "Accept: application/json" \
-H "Content-Type: application/json" \
-X POST \
-d '{ "key": "value", "id": 1, "recipients": ["[email protected]", "[email protected]"] }' \
http://api.yoursite.com
# Flags:
# -I option to only display response headers
# -H option to send custom request headers
# -X option specifies the method
Media types specify the scheme for resource representations.
class ZombiesController < ApplicationController
def index
zombies = Zombie.all
respond_to do |format|
format.json { render json: zombies, status: 200 }
format.xml { render xml: zombies, status: 200 }
end
end
end
# OR using respond_with
class UsersController < ApplicationController
respond_to :json, :xml
def index
@users = User.all
respond_with(@users)
end
end
Rails ships with 21 different media types out of the box. Such as HTML, CSV, PNG, JSON, PDF, ZIP, and many more.
To see all media types Rails ships with:
Mime::SET.collect(&:to_s)
A couple of things are expected from a successful POST request:
- The status code for the response should be
201 - Created
- The response body should contain a representation of the new resource
- The
Location
header should be set with the location of the new resource
def create
episode = Episode.new(episode_params)
if episode.save
render json: episode, status: 201, location: episode
end
end
Rails checks for an authenticity token on POST, PUT/PATCH and DELETE.
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with:
end
# config/environments/test.rb
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
Sometimes responses might not need to include a response body. Responses can be made a lot faster with no response body and could help reduce load.
def create
episode = Episode.new(episode_params)
if episode.save
render nothing: true, status: 204, location: episode
end
end
It's more explicit to use the head
method, which creates a response consisting solely of HTTP headers.
def create
episode = Episode.new(episode_params)
if episode.save
head 204, location: episode # or head :no_content
end
end
Unsuccessful requests also need a proper response.
def create
episode = Episode.new(episode_params)
if episode.save
render json: episode, status: :created, location: episode
else
render json: episode.errors, status: 422
end
end
201 - Created
means the request has been fulfilled and resulted in a new resource being created.204 - No Content
means the server has fulfilled the request but does not need to return an entity-body422 - Unprocessable Entity
means the client submitted request was well-formed but semantically invalid.500 - Internal Server Error
means the server encountered an unexpected condition which prevented it from fulfilling the request.
# config/routes.rb
namespace :v1 do
resources :zombies
end
namespace :v2 do
resources :zombies
end
# app/controllers/v1/zombies_controller.rb
module V1
class ZombiesController < ApplicationController
before_action proc { @remote_ip = request.headers['REMOTE_ADDR'] }
def index
render json: "#{@remote_ip} Version One", status: 200
end
end
end
If an app strictly serves as a web API, it's ok to use ApplicationController
as the base class.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action ->{ @remote_ip = request.headers['REMOTE_ADDR'] }
end
Also see http://pivotallabs.com/api-versioning.
TODO
Error Handling Development Tools Postman
Understanding REST Headers and Parameters
Tools: Use Rspec Requests w/ Webmock
Rails API Integration Testing
Rails API Testing Best Practices