API by Eucalyp

Rails projects typically start out as simple HTML pages and forms operating on a database, but evolve into more complex applications over time. In order to add a client-side framework or mobile application, creating a REST API might be necessary.

It’s really straightforward to create a new Rails API. However, it might be easier to add an API to an existing project. There is no one, “right” way to do this, but here are some things to keep in mind that can make future development a bit easier.

Keep API Routes in an API Namespace

By keeping API routes under a specific namespace, it’s easier for other developers to find which URLs are API-specific. It also gives clients a consistent and descriptive URL pattern.

In config/routes.rb:

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :things
    end
  end
end

This results in the following routes:

       Prefix Verb   URI Pattern                     Controller#Action
api_v1_things GET    /api/v1/things(.:format)        api/v1/things#index
              POST   /api/v1/things(.:format)        api/v1/things#create
 api_v1_thing GET    /api/v1/things/:id(.:format)    api/v1/things#show
              PATCH  /api/v1/things/:id(.:format)    api/v1/things#update
              PUT    /api/v1/things/:id(.:format)    api/v1/things#update
              DELETE /api/v1/things/:id(.:format)    api/v1/things#destroy

Another tip is to version the API with v1 right off the bat, which allows it to grow into a v2 if needed.

Create New API Controllers Under an API Module

With the routes in a separate namespace, all API-specific logic can go into new controllers:

module Api
  module V1
    class ThingsController < ApplicationController
    end
  end
end

They go into a similar module structure to how the routes are drawn:

▾ app/
  ▾ controllers/
    ▾ api/
      ▾ v1/
        things_controller.rb

Creating new controllers, rather than adding API logic to the existing web controllers, keeps them (and their tests) smaller and more cohesive. API controllers generally have to handle:

These are rarely needed in the existing web controllers, which handle more browser-centric concerns (ex. HTML templates).

Use an API-Specific Base Controller

The new controllers can probably get away with inheriting from ApplicationController. However, it’s beneficial and easy to create a separate base controller:

module Api
  module V1
    class BaseController < ApplicationController
    end
  end
end

This keeps shared, API-specific logic in its own place, like token authentication or JSON error-message formatting.

Share Business Logic via Service Objects

Logic between API and web controllers eventually overlap. This is where design patterns can really help keep code as DRY as possible.

Service objects can be used to share logic between controllers:

For example, querying data is typically the same in the index actions of both API and web controllers:

class ThingsController < ApplicationController
  def index
    @things = Thing.where(user_id: current_user.id)
                   .paginate(page: params[:page], per_page: 10)
                   .order(created_at: :desc)
  end
end
module Api
  module V1
    class ThingsController < BaseController
      @things = Thing.where(user_id: current_user.id)
                     .paginate(page: params[:page], per_page: 10)
                     .order(created_at: :desc)
      render json: @things
    end
  end
end

By extracting this logic into a “query” object:

class ThingQuery
  def index(user:, page:)
    Thing.where(user: user.id)
         .paginate(page: page, per_page: 10)
         .order(created_at: :desc)
  end
end

Both controllers can share the same logic:

class ThingsController < ApplicationController
  def index
    @things = ThingQuery.new.index(user: current_user, page: params[:page])
  end
end
module Api
  module V1
    class ThingsController < BaseController
      @things = ThingQuery.new.index(user: current_user, page: params[:page])
      render json: @things
    end
  end
end

This tip isn’t specific controllers. In fact, service objects can be beneficial throughout an entire Rails codebase.

Conclusion

Before adding a new REST API to an existing Rails project, keep these tips in mind. They aren’t hard and fast rules, but they might make development easier in the future.

Questions? Comments? Critiques? Leave me a comment below!

Landing image by Eucalyp from Flaticon