How to Make WWWow with
star image
Jamstack / Headless
A free guide!

Rails on a Diet

More and more apps are being created with an API support. Growing popularity of Angular.js and Backbone.js makes it even more important part of new projects. Why is that? There is a trend to seperate backend from frontend not only in terms of logic but also to split the project into two seperate ones - pure API backend and client-side-only frontend. What’s all the fuss about?

railsondiet_

More and more apps are being created with an API support. Growing popularity of Angular.js and Backbone.js makes it even more important part of new projects. Why is that? There is a trend to seperate backend from frontend not only in terms of logic but also to split the project into two seperate ones - pure API backend and client-side-only frontend. What's all the fuss about? It is simple - you have a nice and clear API for a javascript webapplication and therefore you do not need to worry about the assets pipeline and other full-stack projects related issues. And you get an API for mobile devices. For free.

Rails is a huge framework. It supports almost everything you need and more. You get nice templating engine with partials, cookies and flashes support, static assets and other goodies. However, when you are developing a pure API backend you do not need most of these.

Rails-api

There is a nice gem called rails-api which is sufficient for most of your needs. You can grab more details on this from this railscast. Here we want to digg into the internals of the default rails configuration and tune it for API purposes so we won't use rails-api gem.

Middlewares

Rails applications have few middlewares enabled by default. For Rails 4.1.4 these are:

    Rack::Sendfile
    ActionDispatch::Static
    Rack::Lock
    #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ff931376400>
    Rack::Runtime
    Rack::MethodOverride
    ActionDispatch::RequestId
    Rails::Rack::Logger
    ActionDispatch::ShowExceptions
    ActionDispatch::DebugExceptions
    BetterErrors::Middleware
    ActionDispatch::RemoteIp
    ActionDispatch::Reloader
    ActionDispatch::Callbacks
    ActionDispatch::Cookies
    ActionDispatch::Session::CookieStore
    ActionDispatch::Flash
    ActionDispatch::ParamsParser
    Rack::Head
    Rack::ConditionalGet
    Rack::ETag
    YouApp::Application.routes

You can check enabled middlewares on your own by running rake middleware. As you can see almost all of these are classes. Only LocalCache is an object so you wll have a different address here.

We won't need all of these so let's get rid of the unnecessary ones. It is a good thing to keep middlewares configuration in a seperate file so we create config/middlewares.rb and put the following code in here:

    # load rb files from app/middlewares directory
    # these are our custom middlewares
    Dir[Rails.root.join('app/middlewares/*.rb')].each{|f| require f}

    Rails.application.config.tap do |config|
      # disable unnecessary default middlewares
      config.middleware.delete ::Rack::Sendfile
      config.middleware.delete ::ActionDispatch::Static
      config.middleware.delete ::ActionDispatch::RequestId
      config.middleware.delete ::ActionDispatch::Cookies
      config.middleware.delete ::ActionDispatch::Session::CookieStore
      config.middleware.delete ::ActionDispatch::Flash
      config.middleware.delete ::ActionDispatch::Http::Headers
      config.middleware.delete ::Rack::ETag

      # remove headers: X-Frame-Options, X-XSS-Protection, X-Content-Type-Options
      config.action_dispatch.default_headers = {}
    end

You can drop a different set of middlewares if you want to. Next, we need to load this file from config/environment.rb

# load middlewares config file
require File.expand_path('..\/middleware', __FILE__)

Now, you can put your custom middlewares in app/middlwares directory. But remember to load them from config/middleware.rb e.g. if we want not to return X-Runtime header in every single response we need to manually hide it using a custom middleware. Deleting Rack::Runtime will fail in this case. In this situation you should insert your hiding middleware using:

    # remove X-Runtime header
    config.middleware.insert_before(::Rack::Runtime, ::HideRuntime)

The code for app/middlewares/hide_runtime.rb may look like this:

    class HideRuntime
      def initialize(app)
        @app = app
      end
      def call(env)
        status, headers, body = @app.call(env)
        headers.delete('X-Runtime')
        [status, headers, body]
      end
    end

Rails frameworks

OK, so we have dropped some unnecessary middlewares so far. The next thing to consider is to choose which of the default Rails frameworks we actually need. Let's take a look at config/application.rb.

    require File.expand_path('../boot', __FILE__)

    # Pick the frameworks you want:
    require 'active_model/railtie'
    require 'active_record/railtie'
    require 'action_controller/railtie'
    require 'action_mailer/railtie'
    require 'action_view/railtie'
    require 'sprockets/railtie'
    require 'rails/test_unit/railtie'

    # Require the gems listed in Gemfile, including any gems
    # you've limited to :test, :development, or :production.
    Bundler.require(*Rails.groups)

    module MyApp
      class Application < Rails::Application
        # Settings in config/environments/* take precedence over those specified here.
        # Application configuration should go into files in config/initializers
        # -- all .rb files in that directory are automatically loaded.

        # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
        # Run 'rake -D time' for a list of tasks for finding time zone names. Default is UTC.
        # config.time_zone = 'Central Time (US & Canada)'

        # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
        # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
        # config.i18n.default_locale = :de
      end
    end

You can freely comment out some of require statements. We will not need action_view orsprocets. If you prefer RSpec you can also drop test_unit. ActiveRecord is kinda fat so if you are going to use some NoSQL database you should check out MongoDB and its Mongoid framework. For SQL databases check out the DataMapper framework. Mongoid still depends on ActiveModel so we can leave it uncommented. If you are not planning to send emails from within your application then you should comment action_mailer and remember to drop config statements from files in config/environments directory. Finally we can end up with something like this:

    # Pick the frameworks you want:
    require 'active_model/railtie'
    # require 'active_record/railtie'
    require 'action_controller/railtie'
    # require 'action_mailer/railtie'
    # require 'action_view/railtie'
    # require 'sprockets/railtie'
    # require 'rails/test_unit/railtie'

Gemfile

In your Gemfile do not hestitate to use groups. Every gem which is essential only in development and/or test should be put in its group e.g. capistrano or pry-byebug should be wrapped in development group

    group :development do
      gem 'better_errors'
      gem 'binding_of_caller'
      gem 'pry-byebug'
      gem 'capistrano'
      # ...
    end

This will prevent from loading extra code in your staging/production environments. Also feel free to use require: false for gems you do not want to be automatically loaded from your config/application.rb file.

    gem 'koala', require: false

It means that if you want to use that gem you should first require it on top of your file - like in pure ruby code. It may be inconvinient but should speed up your application.

Api Controller

The most important part of your application. Since it's an API project we don't call it ApplicationController. It's just ApiController. First, don't declare this class as inherited from ActionController::Base. We don't want that. We don't need that. Use ActionController::Metal instead which is more lightweight and add only the modules you need.

OK, so now you have two strategies to follow. You can either include what you want or exclude what you don't want. I personally prefer the second approach. Notice that if you're using rabl or jbuilder templates you probably want to have rendering stuff included. Your ApiController should be something like this:

    module Api
      class ApiController < ActionController::Metal
        WITHOUT = [
          AbstractController::Translation,
          AbstractController::AssetPaths,
          ActionController::UrlFor,
          ActionController::Redirecting,
          ActionController::Renderers::All,
          ActionController::ConditionalGet,
          ActionController::Caching,
          ActionController::MimeResponds,
          ActionController::Cookies,
          ActionController::Flash,
          ActionController::RequestForgeryProtection,
          ActionController::ForceSSL,
          ActionController::Streaming,
          ActionController::DataStreaming,
          ActionController::HttpAuthentication::Basic::ControllerMethods,
          ActionController::HttpAuthentication::Digest::ControllerMethods,
          ActionController::HttpAuthentication::Token::ControllerMethods,
          ActionController::Instrumentation,
          ActionController::ParamsWrapper
        ]

        ActionController::Base.without_modules(*WITHOUT).each do |el|
          include el
        end

        prepend_view_path 'app/views'
      end
    end

If ou want your views to load correctly remember to set the view path as above. Keep in mind that if you don't like excluding approach you can always take a look into ActionController::Base class and include only those modules you really need. Here's the array of all the modules included in ActionController::Base by default:

    MODULES = [
      AbstractController::Rendering,
      AbstractController::Translation,
      AbstractController::AssetPaths,

      Helpers,
      HideActions,
      UrlFor,
      Redirecting,
      ActionView::Layouts,
      Rendering,
      Renderers::All,
      ConditionalGet,
      RackDelegation,
      Caching,
      MimeResponds,
      ImplicitRender,
      StrongParameters,

      Cookies,
      Flash,
      RequestForgeryProtection,
      ForceSSL,
      Streaming,
      DataStreaming,
      HttpAuthentication::Basic::ControllerMethods,
      HttpAuthentication::Digest::ControllerMethods,
      HttpAuthentication::Token::ControllerMethods,

      # Before callbacks should also be executed the earliest as possible, so
      # also include them at the bottom.
      AbstractController::Callbacks,

      # Append rescue at the bottom to wrap as much as possible.
      Rescue,

      # Add instrumentations hooks at the bottom, to ensure they instrument
      # all the methods properly.
      Instrumentation,

      # Params wrapper should come before instrumentation so they are
      # properly showed in logs
      ParamsWrapper
    ]

Dependencies

Whan using the described setup you should be aware of the fact that you can get errors if some of your gems are using something you dropped from your application. As an example you may take Draper (decorators framework) which won't work untill you have ActionDispatch::RequestId middleware laoded.

Next?

From now on it's all up to you. If you've chosen only those middlewares and frameworks you need your application should be lighter, cleaner and even faster comapring to full Rails setup.

That's all for now. Till next time.

Let's talk about Jamstack and headless e-commerce!

Contact us and we'll warmly introduce you to the vast world of Jamstack & headless development!

GET AN ESTIMATE