This Rails pattern provides a flexible way for clients to specify whether they want nested resources includes in the API response simply by passing a flag as a parameter to the endpoint.
For example if you were building a simple blog, you would get Posts with Comments by calling posts.json?comments=true
, however calling posts.json
or posts.json?comments=false
would return Posts without Comments.
posts.json?comments=false
posts.json?comments=true
This is incredibly powerful giving the client the flexibility to consume lighter weight responses by default or make less requests by including nested resources when needed.
The trick to avoid N+1 queries is to use a scope which calls includes(:comments)
when the flag is true. To achieve this the Posts and Comments models would be something like this.
class Post < ApplicationRecord
belongs_to :user, counter_cache: true
has_many :comments, -> { order(created_at: :desc)}, inverse_of: :post, dependent: :destroy
scope :with_comments, ->(include) { includes(:comments) if include.present? && include.to_bool }
end
class Comment < ApplicationRecord
belongs_to :user, counter_cache: true
belongs_to :post, counter_cache: true
end
The routes.rb
would look something like the following.
Rails.application.routes.draw do
resources :posts do
resources :comments
end
end
Your Posts controller would be as such.
class PostsController < ApplicationController
def index
comments = params.fetch(:comments, nil)
@posts = Post.with_comments(comments).all
end
end
Your _post.json.jbuilder
would then look like the following.
json.extract! post, :id, :title, :body, :user_id, :created_at, :updated_at
if params.fetch(:comments, false).to_bool
json.comments post.comments do |comment|
json.partial! "comments/comment", comment: comment
end
end
Finally I like to include the following to_bool.rb
in my initializers so I can call to_bool
throughout my code.
class Object
def to_bool
!!self
end
end
class Integer
def to_bool
self == 1
end
end
class String
def to_bool
if self == true || self =~ /(true|t|yes|y|1|on)$/i
true
elsif self == false || self.blank? || self =~ /(false|f|no|n|0|off)$/i
false
else
false
end
end
end
class Symbol
def to_bool
self.downcase.to_s =~ /(true|t|yes|y|1|on)$/i
end
end
I made the example overly simple so it was easy to follow, however this same pattern could be used for belongs_to resources or multiple has_many
resources too.
It can also work with multiple level of nested resources. For example if you wanted to include User in the Posts and Comments, you would just need to add with_user scope to your models, update your controller to use this scope, then update your json.jbuilder
to render User when the flag is set.
class Post < ApplicationRecord
belongs_to :user, counter_cache: true
has_many :comments, -> { order(created_at: :desc)}, inverse_of: :post, dependent: :destroy
scope :with_user, ->(include) { includes(:user) if include.present? && include.to_bool }
scope :with_comments, ->(include) { includes(:comments) if include.present? && include.to_bool }
end
class Comment < ApplicationRecord
belongs_to :user, counter_cache: true
belongs_to :post, counter_cache: true
scope :with_user, ->(include) { includes(:user) if include.present? && include.to_bool }
end
In your Posts controller youโd need to call with_user
scope.
class PostsController < ApplicationController
def index
user = params.fetch(:user, nil)
comments = params.fetch(:comments, nil)
@posts = Post.with_user(user).with_comments(comments).all
end
end
Then update _post.json.jbuilder
to include the User.
json.extract! post, :id, :title, :body, :user_id, :created_at, :updated_at
if params.fetch(:user, false).to_bool
json.user post.user, partial: 'users/user', as: :user
end
if params.fetch(:comments, false).to_bool
json.comments post.comments do |comment|
json.partial! "comments/comment", comment: comment
end
end
posts.json?comments=false&user=true
posts.json?comments=true&user=true
Just a note, if your nested resources have a lot of results, you may want to limit the number of resources being returned, for example only include the last 10 Comments.
json.extract! post, :id, :title, :body, :user_id, :created_at, :updated_at
if params.fetch(:comments, false).to_bool
json.comments post.comments.limit(10) do |comment|
json.partial! "comments/comment", comment: comment
end
end
Or optionally use an integer instead of a boolean so you can pass the number of nested resources youโd like to receive. For example posts.json?comments=10
could return 10 most recent Comments.
I love this pattern because it doesnโt take a lot of work to setup in Rails yet still generates efficient queries but is incredibly flexible and powerful for the consuming client giving them the option to tailor the JSON responses based on their needs.
Would this pattern help your Rails project? Have you used a similar strategy before? Iโd love to hear from you in the comments!
Top comments (0)