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?
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!