Join us
API with Ruby on Rails

API with Ruby on Rails: useful tricks

Last updated February 14, 2025 4 min read

This article is about gotchas you should be aware of while building your API with Ruby on Rails.

Controller tricks: API on Metal

Sooner or later each Rails developer come to a point when he wants to build his first API.
Among the first things you have to take care of are your controllers.
If you want your API to be fast (and I bet you do) then you should consider using ActionController::Metal.
The trick is that ActionController::Base has many Middlewares that are not necessary for the API, by using Metal controller with minimum modules included, the one can achieve up to 40% speedup.
Lets see what your basic metal controller may looks like:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Api::V1::BaseController < ActionController::Metal
include ActionController::Rendering # enables rendering
include ActionController::MimeResponds # enables serving different content types like :xml or :json
include AbstractController::Callbacks # callbacks for your authentication logic
append_view_path "#{Rails.root}/app/views" # you have to specify your views location as well
end
class Api::V1::BaseController < ActionController::Metal include ActionController::Rendering # enables rendering include ActionController::MimeResponds # enables serving different content types like :xml or :json include AbstractController::Callbacks # callbacks for your authentication logic append_view_path "#{Rails.root}/app/views" # you have to specify your views location as well end
class Api::V1::BaseController < ActionController::Metal
  include ActionController::Rendering        # enables rendering
  include ActionController::MimeResponds     # enables serving different content types like :xml or :json
  include AbstractController::Callbacks      # callbacks for your authentication logic

  append_view_path "#{Rails.root}/app/views" # you have to specify your views location as well
end

Unfortunately NewRelic doesn’t support Metal by default, so you have to add monitoring manually.

Routing: use versioning

Nobody’s perfect. So are we.
Your API will definitely be changed and extended multiple times in the future so you better take care of your versioning at the beginning.
As you noticed, BaseController wrapped in V1 namespace. Use something like this in your routes:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
namespace :api do
namespace :v1 do
# put your routes here
end
end
namespace :api do namespace :v1 do # put your routes here end end
namespace :api do
  namespace :v1 do
    # put your routes here
  end
end

Views tricks: RABL ’em all

You don’t want to burden your code with logic of exposing different model fields for different API actions, right?
In this case you should use some template engine. RABL is at your service. Here’s an example of your view:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
object @object
attribute :public_id => :id
attributes :title, :created_at, :source
child :contacts do
attributes :title
end
child :files do
attributes :filename, :content_type, :size
end
object @object attribute :public_id => :id attributes :title, :created_at, :source child :contacts do attributes :title end child :files do attributes :filename, :content_type, :size end
object @object

attribute :public_id => :id
attributes :title, :created_at, :source

child :contacts do
  attributes :title
end

child :files do
  attributes :filename, :content_type, :size
end

Also that will save you time by getting rid of ugly respond_to blocks. Instead of

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
def show
@object = Object.find(params[:id])
respond_to do |f|
f.json { render json: @object.to_json }
f.xml { render xml: @object.to_xml }
end
end
def show @object = Object.find(params[:id]) respond_to do |f| f.json { render json: @object.to_json } f.xml { render xml: @object.to_xml } end end
def show
  @object = Object.find(params[:id])
  respond_to do |f|
    f.json { render json: @object.to_json }
    f.xml  { render xml: @object.to_xml }
  end
end

You can simple do literally Nothing

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
def show
@object = Object.find(params[:id])
end
def show @object = Object.find(params[:id]) end
def show
  @object = Object.find(params[:id])
end

Just make sure you have a RABL view in a corresponding directory.

Security

There’re plenty of articles about API security best practices with OAuth and OIDC. Another convenient way is to simply use token passed in the query string. If you ended up with token keep in mind that you can easily generate it by calling SecureRandom.urlsafe_base64. To make sure token is unique you can use something like this:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
def build_token
begin
token = SecureRandom.urlsafe_base64
end while User.exists?(api_token: token)
token
end
def build_token begin token = SecureRandom.urlsafe_base64 end while User.exists?(api_token: token) token end
def build_token
  begin
    token = SecureRandom.urlsafe_base64
  end while User.exists?(api_token: token)
  token
end

Hiding your IDs with GUIDs

By default Rails uses incremental integer for primary key.
Common practice is not to expose these kind of IDs to the public via your API because users can guess other IDs in your database and that might be a potential risk.
To solve this you can come up with a simple algorithm that will convert your IDs into some “safe” form and back.
But still it’s not super safe because someone can find out what the algorithm is.
Another possible solution is to expose GUIDs to the public.
It’s a 128 entity that typically looks like this:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11

To generate it in Ruby use SecureRandom.uuid (generates V4 GUID).
You can store it as a simple string column but if you are using Postgresql then you can utilize it’s built in uuid type – this can save you a lot of space.
Rails, however falls back to :string for the uuid Postgresql native type.
To workaround this you can create column manually in your migration (index should be added as well):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
execute "ALTER TABLE objects add COLUMN public_id uuid NOT NULL DEFAULT uuid_generate_v4()"
add_index :objects, [:public_id], name: "index_objects_on_public_id", unique: true
execute "ALTER TABLE objects add COLUMN public_id uuid NOT NULL DEFAULT uuid_generate_v4()" add_index :objects, [:public_id], name: "index_objects_on_public_id", unique: true
execute "ALTER TABLE objects add COLUMN public_id uuid NOT NULL DEFAULT uuid_generate_v4()"
add_index :objects, [:public_id], name: "index_objects_on_public_id", unique: true

To use uuid_generate_v4 function you have to add Postgresql extension:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
create extension "uuid-ossp"
create extension "uuid-ossp"
create extension "uuid-ossp"

And don’t forget to change your schema format to :sql in config/application.rb

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
config.active_record.schema_format = :sql
config.active_record.schema_format = :sql
config.active_record.schema_format = :sql

Testing your API

You definitely want to cover up your new shiny API with some sort of tests. See example below that uses Rack::Test::Methods

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# spec_helper
module ApiHelper
include Rack::Test::Methods
def app
Rails.application
end
end
RSpec.configure do |config|
config.include ApiHelper, api: true
end
# spec
require 'spec_helper'
describe "api/v1/objects", api: true do
let(:url) { "api/v1/objects" }
let(:object) { Factory.create(:object) }
let(:token) { "YOUR_SECRET_TOKEN" }
let(:data) { JSON.parse(last_response.body) }
before(:each) { get url, token: token } # here's where API call made
subject { data }
it { should have(1).object }
end
# spec_helper module ApiHelper include Rack::Test::Methods def app Rails.application end end RSpec.configure do |config| config.include ApiHelper, api: true end # spec require 'spec_helper' describe "api/v1/objects", api: true do let(:url) { "api/v1/objects" } let(:object) { Factory.create(:object) } let(:token) { "YOUR_SECRET_TOKEN" } let(:data) { JSON.parse(last_response.body) } before(:each) { get url, token: token } # here's where API call made subject { data } it { should have(1).object } end
# spec_helper
module ApiHelper
  include Rack::Test::Methods

  def app
    Rails.application
  end
end

RSpec.configure do |config|
  config.include ApiHelper, api: true
end

# spec
require 'spec_helper'

describe "api/v1/objects", api: true do
  let(:url) { "api/v1/objects" }
  let(:object) { Factory.create(:object) }
  let(:token) { "YOUR_SECRET_TOKEN" }
  let(:data) { JSON.parse(last_response.body) }

  before(:each) { get url, token: token }       # here's where API call made

  subject { data }

  it { should have(1).object }
end

Conclusion

We’ve just gone through the following tricks:

  • Minimum usable Metal controller
  • Simple versioning
  • Easy API views with RABL
  • Postresql native uuid type
  • API spec sample

Sure, you’ll face a lot more issues in the wild but these basics intended to help you start up quickly.
The last, but not the least – don’t forget about good documentation. Your API users will definitely appreciate it.

If you want to try all these tricks in the wild see recently released API for our dummy SMTP server – Mailtrap.io

UPDATE
If you want to use render json: your_json you should include ActionController::Renderers::All in your base API controller. Thanks Divya Kunnath for pointing that out.

We are damn good at building products

More about collaboration