Skip to content

Instantly share code, notes, and snippets.

@dideler
Forked from rosiehoyem/apis
Last active January 6, 2024 15:13
Show Gist options
  • Save dideler/58041566028ac0f23921 to your computer and use it in GitHub Desktop.
Save dideler/58041566028ac0f23921 to your computer and use it in GitHub Desktop.
From Code School's Zombies series

API Best Practices

Routes

Restricting routes

resources :zombies, only: [:index, :show]
resources :humans, except: [:destroy, :edit, :update] 

Subdomains

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

Nicer URLs for local development

# /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

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

Requests

GET Requests

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

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

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)

POST Requests

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

Request Authentication

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

Bodyless responses

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

Other types of responses

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-body
  • 422 - 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.

Versioning

Versioning Using the URI

# 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

Documentation

Testing

Tools: Use Rspec Requests w/ Webmock

Rails API Integration Testing

Rails API Testing Best Practices

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment