DEV Community

Dale Zak
Dale Zak

Posted on • Originally published at dalezak.Medium

Rails Pattern For Including Nested Resources In API Responses

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.

1_qMMhj-G7mh42n8C-oIGqQQ

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

1_yG3o1atd_48IvoPAKydqcA

posts.json?comments=true

1_lF4bUjqA7ePj4oCvWt6MiA

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
Enter fullscreen mode Exit fullscreen mode

The routes.rb would look something like the following.

Rails.application.routes.draw do
 resources :posts do
  resources :comments
 end
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
posts.json?comments=false&user=true

1_NsFKA_O6pwA7kMbKlgJY3w

posts.json?comments=true&user=true

1_tm2elhkZkFo1wUGU7gGuRQ

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
Enter fullscreen mode Exit fullscreen mode

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)