Ruby on Rails

Crafting A RESTful Ruby on Rails API

APIs have become a major cornerstone of Internet systems, as the interconnectivity of web applications, brick-and-mortar businesses, and services have dramatically increased over the last decade. API's can bolster a business's relevance and long-term survival and can often be a new source of income. But to be successful, you need a well-designed API that is designed for optimum return on investment. A poorly designed API can lead to costly support calls from API customers as they inquire about help integrating, or complain about API changes that you made, breaking their own costly integrations--they often will hire developers to do a custom integration--thwarting their ability to access data. To optimize your API, its needs to be RESTful and, in this guide, we'll use Ruby on Rails, which was originally designed for API usage from the start.

What is a RESTful API?

A RESTful API is an application program interface (API) that accepts HTTP GET, PUT, POST and DELETE data requests. REST is acronym for REpresentational State Transfer. The general architectural design of REST is:

  • Uniformality: The design is uniform throughout. Each resource should be individually accessible, with no resource object too large, or if not better suited to be accessed by separate calls. Getting resources should have common approaches such that all resources are accessed and updated similarly.
  • Cient-Server: Accessing an API should not have dependencies--the access should be simple with the ability to make changes on either the client or server, but that the bridge between them remains consistent and non-brittle.
  • Stateless: There is no state stored on the server between requests, without use of sessions or cookies. Each request is considered individual with no connection to any prior or future request;
  • Unique resources per request: Each request should uniquely identify a single resource, and can list or update zero or more resource objects;
  • Representational state transfer: The API should return a representation of resource date, typically in JSON or XML.

Rest Architectural Constraints has a good explanation of REST.

Setup: Example Blog Application

Create a New Application

To get started, we will create a blog application with resources that will be available via API. While it is possible to offer JSON responses alongside default HTML responses, we will create a subset of versioned controllers that are exclusive to the API, which provides a clear distinction within the code and is simpler to work with in the long term. We will use the Full Rails App instructions below, which you can add on additional views that might be available outside of the API, but you can also use the API Only instructions if that is your focus.

# Standard Ruby on Rails application

$ rails new blog
$ cd blog
$ bundle


# API-only Ruby on Rails application

$ rails new blog --api
$ cd blog
$ bundle

The API-only instructions will:

  • Configure your application to start with a more limited set of middleware than normal, exlcuding any middleware primarily useful for browser applications (like sessions and cookies support) by default.

  • Make ApplicationController inherit from ActionController::API instead of ActionController::Base. As with middleware, this will leave out any Action Controller modules that provide functionalities primarily used by browser applications.

  • Configure the generators to skip generating views, helpers and assets when you generate a new resource.

One downside to using the API-only approach is that if you want to add an admin interface or views outside of an API, you'll have to manually add back and modify your application setup for this to work, a real pain.

Controllers

Create the directory for our API controllers:

$ mkdir -p app/controllers/api/v1

Base controller with Authentication, Error handling

Create a Base Controller that all of our API controllers will inherit from, and we'll add behaviors that we want all of our API controllers to utilize. In this controller, which will apply to ALL API requests, we'll:

  • Check that requests need to be in JSON;
  • Check that requests are authenticated;
  • Catch all 'Not Found' requests to the API and render an error message;
  • Have a common method render_errors for rendering errors
#  app/controller/api/v1/base_controller.rb

class Api::V1::BaseController < ApplicationController
  before_action :ensure_json_request
  before_action :verify_token!
  skip_before_action :verify_authenticity_token

  def render_not_found
    render_errors([{ code: '5', message: 'Resource not found' }], :not_found)
  end

  private

  def ensure_json_request  
    return if request.format == :json
    request.format.symbol == :json
    render_errors([{ code: '1', message: 'Requests need to be in JSON format.'}], :not_acceptable)#406
  end

  def verify_token!
    unless params[:token] == 'secretkey'
      render_errors([{ code: '2', message: 'Unauthorized'}], :unauthorized)#401
    end
  end

  def render_errors(errors=[], status=:ok)
    render json: { errors: errors }, status: status
  end
end

API Resources

Post Model

Create the Post model and migrate it to the database:

$  rails g model Post title content:text publication_date:date author
$  rails db:migrate

Post Controller

Create our Posts controller:

# posts_controller.rb

class Api::V1::PostsController < Api::V1::BaseController
  before_action :get_post, only: [:show, :update, :destroy]

  def index
    posts = Post.all
    render json: { posts: posts }, status: :success
  end

  def show
    render json: @post, status: :success
  end

  def update
    if @post.update(post_params)
      render json: @post, status: :ok
    else
      render_errors(@post.errors, :unprocessable_entity)
    end
  end

  def create
    post = Post.new(post_params)
    if post.save
      render json: post, status: :created
    else
      render_errors(@post.errors, :unprocessable_entity)
    end
  end

  def destroy
    if @post && @post.destroy
      render json: { message: 'Post successfully destroyed' }, status: :no_content
    else @post
      render_errors(@post.errors, :unprocessable_entity)
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :content, :publication_date, :author)
  end

  def get_post
    @post =  Post.find_by(id: params[:id])
    unless @post 
      render_errors([{code: '3', message: 'Post not found'}], :not_found)
    end
  end
end

Routes

# routes.rb

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :posts
      get '*path', to: 'base#render_not_found'
    end
  end
end

With the routes in place, we can see that our routes exist:

$ rake routes
                   Prefix Verb   URI Pattern                                                                              Controller#Action
             api_v1_posts GET    /api/v1/posts(.:format)                                                                  api/v1/posts#index
                          POST   /api/v1/posts(.:format)                                                                  api/v1/posts#create
          new_api_v1_post GET    /api/v1/posts/new(.:format)                                                              api/v1/posts#new
         edit_api_v1_post GET    /api/v1/posts/:id/edit(.:format)                                                         api/v1/posts#edit
              api_v1_post GET    /api/v1/posts/:id(.:format)                                                              api/v1/posts#show
                          PATCH  /api/v1/posts/:id(.:format)                                                              api/v1/posts#update
                          PUT    /api/v1/posts/:id(.:format)                                                              api/v1/posts#update
                          DELETE /api/v1/posts/:id(.:format)                                                              api/v1/posts#destroy

Using the API

Start the server up:

$ rails server

Authentication

The authentication in our application is basic token authentication, requiring a secret token ('secretkey') that must be passed in the params on every request.

   http://localhost:3000/api/v1/posts.json?token=secretkey

We check for the token in BaseController. Without the token, you will get a 401 Unauthorized HTTP response:

{"errors":[{"code":"2","message":"Unauthorized"}]}

Create a Post

To create a post, we will submit a HTTP POST with the params of a post object, and include the necessary authentication token.

Request

POST http://localhost:3000/api/v2/posts.json?token=secretkey

With a body type of application/json and content:

{
  "post": {
    "title": "Title of a Blog post",
    "content": "This is the content",
    "author": "John Doe"
  }
}

Response

This will get a 201 Created response and content:

{
  "id": 1,
  "title": "Title of a Blog post",
  "content": "This is the content",
  "publication_date": null,
  "author": "John Doe",
  "created_at": "2019-01-15T16:25:15.557Z",
  "updated_at": "2019-01-15T16:25:15.557Z"
}

List Posts

And then we can try it out in the browser with a simple GET of posts:

Request

GET http://localhost:3000/api/v1/posts.json?token=secretkey

This will result in the following response with the one post we created:

Response

{
"posts": [
      {
      "id": 1,
      "title": "Title of a Blog post",
      "content": "This is the content",
      "publication_date": null,
      "author": "John Doe",
      "created_at": "2019-01-15T16:25:15.557Z",
      "updated_at": "2019-01-15T16:25:15.557Z"
    }
  ],
}

Update a Post

If we want to update our post, we can:

Request

PATCH http://localhost:3000/api/v1/posts/1.json?token=secretkey

With body content:

{
    "post": {
      "title": "Exciting New Title"
    }
}

Response

We will get a 200 OK response and an updated post:

{
  "id": 1,
  "title": "Exciting New Title",
  "content": "This is the content",
  "publication_date": null,
  "author": "John Doe",
  "created_at": "2019-01-15T16:25:15.557Z",
  "updated_at": "2019-01-15T16:29:53.563Z"
}

Deleting a Post

To delete our post, we:

Request

DELETE http://localhost:3000/api/v1/posts/1.json?token=secretkey

Response

This will delete the post and return a 204 No Content.

Handling Errors

Providing insightful error messages is important in API design as developers design integrations and looks for helpful clues when they are troubleshooting an API. At a minimum, responses should contain an accurate HTTP response code, ie. a 201 created code for a successfully created object, and a 422 unprocessable entity code if on creation an object is invalid.

In addition to informative response codes, you can provide detailed Ruby on Rails validation errors, messages and or a custom error codes to provide even more information. REST design doesn't dictate response bodies, so take an approach and be consistent across the API. In our approach, we provide an errors array object, to hold any number of errors, with a code that could point to a dictionary of custom error codes that could provide additional information.

Catchall Error for Not Found Requests

We also need to capture routes that don't exist within our API. We do this with a catchall route in routes.rb:

  get '*path', to: 'base#render_not_found'

Normally, you would get the standard HTML not found Rails response, which would be lots of HTML, so by doing this, we are returning a JSON response with a clear and uncluttered 'not found' message.

Error Codes

Error codes are really useful for providing more specific info and when provided to Support, they could substantially shorten the time to narrow down and identify a problem. In our sample application, we'll provide these codes:

   CODE    DESCRIPTION
    1       Not acceptable. Request needs to be in JSON.     
    2       Unauthorized. Incorrect or missing correct Token.
    3       Not found. This Post does not exist.
    4       Not found. Resource does not exist.

We importantly, keep this errors object consistent across the API, so that any consuming app could program for it and utilize it effectively.

Modifying Response Objects

Often you'll want to exclude certain attributes in an object or add additional related objects. With our Posts for example, if we don't want to send the updated_at field and we want to include the is_published? method with the object response, we can modify posts_controller accordingly:

# posts_controller.rb

  def show
    render json: @post.as_json(except: [:updated_at], methods: [:is_published?]), status: :success #200
  end

When requesting the post, it will now exclude the updated_at attribute and it will include the is_published? method.

Bottom line: Keep it simple, and don't change it!

Here are key tenants for overall API design:

  • Provide complete access (all of the same access they may have through a UI) to all of the necessary resources a user may want;

  • Interface should be consistent and well-named so that a user can extrapolate calls to all resources once they know how to access one resource;

  • Never change existing API endpoints, which could break customer integrations without substantial advance notice. Changes can be supplemental in nature, allowing for additional attributes, such that the risk of integration breakage is low, and the customer can optionally decide to utilize the new data. Never remove an endpoint unless absolutely necessary, or if you have given customers ample time to adjust for it.


Carson R Cole