Prevent over-fetching data for REST API in Ruby on Rails

kortirso

Bogdanov Anton

Posted on March 12, 2024

Prevent over-fetching data for REST API in Ruby on Rails

One of the main features of GrahpQL compared to the REST api - GraphQL APIs let clients query the exact data they need, while REST APIs just returns all data described in serializers.

While I prefer using REST API for my rails apps I tried to find solution for optimizing responses. I made some attempts with multiple serializers for different controllers and for the same models, but it leds to mess.

With using jsonapi-serializer I found solution.

Attributes of serializer can be optional with if and proc

class UserSerializer < ApplicationSerializer
  attribute :confirmed, if: proc { |_, params| required_field?(params, 'confirmed') }, &:confirmed?
  attribute :banned, if: proc { |_, params| required_field?(params, 'banned') }, &:banned?
end
Enter fullscreen mode Exit fullscreen mode

During serialization such attributes will be or will not be serialized based on provided params.

Attribute will be serialized if it presents in include_fields or does not present in exclude_fields.

class ApplicationSerializer
  def self.required_field?(params, field_name)
    params[:include_fields]&.include?(field_name) || params[:exclude_fields]&.exclude?(field_name)
  end
end
Enter fullscreen mode Exit fullscreen mode

And this is how part in controllers works

SERIALIZER_FIELDS = %w[confirmed banned].freeze
def index
  render json: {
    user: UserSerializer.new(
      current_user, params: serializer_fields(UserSerializer, SERIALIZER_FIELDS)
    ).serializable_hash
  }, status: :ok
end
Enter fullscreen mode Exit fullscreen mode

Method serializer_fields generates hash with include/exclude attributes based on params and compares it with available attributes from serializer. Later that hash is used in serializer.

def serializer_fields(serializer_class, default_include_fields=[])
  @serializer_attributes = serializer_class.attributes_to_serialize.keys.map(&:to_s)
  return {} if response_include_fields.any? && response_exclude_fields.any?
  return { include_fields: response_include_fields } if response_include_fields.any?
  return { exclude_fields: response_exclude_fields } if response_exclude_fields.any?
  return { include_fields: default_include_fields } if default_include_fields.any?

  {}
end

def response_include_fields
  @response_include_fields ||= params[:response_include_fields]&.split(',').to_a & @serializer_attributes
end

def response_exclude_fields
  @response_exclude_fields ||= params[:response_exclude_fields]&.split(',').to_a & @serializer_attributes
end
Enter fullscreen mode Exit fullscreen mode

This approach allows to achieve:

  • preventing over-fetching data by REST API,

  • tracking required attributes for responses (maybe some of them are not used at all),

  • usually controller could have some .includes for optimization, and based on required attributes such optimizations/additional requests to DB can be skipped.

I hope this approach will help somebody with REST API optimization their Rails apps.

💖 💪 🙅 🚩
kortirso
Bogdanov Anton

Posted on March 12, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related