Go to content
The Codest
  • About Us
  • Services
  • Our Team
  • Case studies
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
  • About Us
  • Services
  • Our Team
  • Case studies
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
2022-02-23
Software Development

Simple Filters in Rails API

Krzysztof Buszewicz

Senior Software Engineer

Simple Filters in Rails API - Image

Are you angry every time you see mutating instance variables in rails controller to filter data? This article is for you. :)

Filters

You have probably seen this before:

# app/controllers/api/v1/things_controller.rb

module API
  module V1
    class ThingsController < BaseController
      def index
        @things = Thing.all
        @things = @things.where(size: params[:size]) if params[:size]
        @things = @things.where('name ILIKE ?', "%#{params[:name_contains]}%") if params[:name_contains]

        render json: @things
      end
    end
  end
end

Why do I consider it to be a bad code? Because it simply makes our controller fat. IMHO we should extract as many logic as we can from controllers and use a purpose-related utils or services. In this case we will implement a generic filter that we will be able to use across many controllers.

But wait, first let's analyse the current code. It can be bad but works, though. We have some initial scope (Thing.all) and then are limiting it if user has passed related parameter. For each filter we actually check if the param was passed and if so, we apply a filter. The second thing is that we don't need to use the ivar, we can use plain old local variables.

Ok, then. Couldn't we use some service object to mutate the initial scope? The execution can look like this:

# app/controllers/api/v1/things_controller.rb

module API
  module V1
    class ThingsController < BaseController
      def index
        scope = Thing.all
        things = Things::IndexFilter.new.call(scope, params)

        render json: things
      end
    end
  end
end

It looks much better now, but of course we have to implement the filter yet. Note that call's signature will be the same for all resources, so we can have some generic class for this task.

# app/services/generic/index_filter.rb

module Generic
  class IndexFilter
    EMPTY_HASH = {}.freeze

    def self.filters
      EMPTY_HASH
    end

    def call(scope, params)
      apply_filters!(self.class.filters.keys, scope, params)
    end

    private

    def apply_filters!(filter_keys, scope, params)
      filter_keys.inject(scope.dup) do |current_scope, filter_key|
        apply_filter!(filter_key, current_scope, params)
      end
    end

    def apply_filter!(filter_key, scope, params)
      filter = fetch_filter(filter_key)
      return scope unless apply_filter?(filter, params)

      filter[:apply].call(scope, params)
    end

    def apply_filter?(filter, params)
      filter[:apply?].call(params)
    end

    def fetch_filter(filter_key)
      self.class.filters.fetch(filter_key) { raise ArgumentError, 'unknown filter' }
    end
  end
end

Seems complicated? Not really - all the magic happens in #apply_filters!. We take the duplicate of the initial scope and apply each filter to it.

When we apply the scope it means we mutate the duplicate of our initial scope. And we expect filters to be implemented as a hash in the self.filters method of a child class. Let's do it.

# app/services/things/index_filter.rb

module Things
  class IndexFilter < Generic::IndexFilter
    FILTERS = {
      size_filter: {
        apply?: ->(params) {
          params[:size].is_a?(String)
        },
        apply: ->(scope, params) {
          scope.where(size: params[:size])
        }
      }.freeze,
      name_contains_filter: {
        apply?: ->(params) {
          params[:name_contains].is_a?(String)
        },
        apply: ->(scope, params) {
          scope.where('name ILIKE ?', "%#{params[:name_contains]}%")
        }
      }.freeze
    }.freeze

    def self.filters
      FILTERS
    end
  end
end

That's it! We have written more code, but the simple filters will look the same way for all the resources. We have cleaned controller from the code responsible of filtering and provided 'specialised' class for this purpose that follows very clear convention.

Ruby Developer Offer

Read more:

Pros and cons of Ruby software development

Rails and Other Means of Transport

Rails Development with TMUX, Vim, Fzf + Ripgrep

Related articles

Software Development

3 Useful HTML Tags You Might Not Know Even Existed

Nowadays, accessibility (A11y) is crucial on all stages of building custom software products. Starting from the UX/UI design part, it trespasses into advanced levels of building features in code. It provides tons of benefits for...

Jacek Ludzik
Software Development

5 examples of Ruby’s best usage

Have you ever wondered what we can do with Ruby? Well, the sky is probably the limit, but we are happy to talk about some more or less known cases where we can use this powerful language. Let me give you some examples.

Pawel Muszynski
Software Development

Maintaining a Project in PHP: 5 Mistakes to Avoid

More than one article has been written about the mistakes made during the process of running a project, but rarely does one look at the project requirements and manage the risks given the technology chosen.

Sebastian Luczak
Software Development

5 reasons why you will find qualified Ruby developers in Poland

Real Ruby professionals are rare birds on the market. Ruby is not the most popular technology, so companies often struggle with the problem of finding developers who have both high-level skills and deep experience; oh, and by the...

Jakub
Software Development

9 Mistakes to Avoid While Programming in Java

What mistakes should be avoided while programming in Java? In the following piece we answers this question.

Rafal Sawicki
Software Development

A quick dive into Ruby 2.6. What is new?

Released quite recently, Ruby 2.6 brings a bunch of conveniences that may be worth taking a glimpse of.  What is new? Let’s give it a shot!

Patrycja Slabosz

Subscribe to our knowledge base and stay up to date on the expertise from industry.

About us

We are an agile software development company dedicated to empowering our clients' digital transformation projects and ensuring successful IT project delivery.

    United Kingdom - Headquarters

  • Office 303B, 182-184 High Street North E6 2JA London, England

    Poland - Local Tech Hubs

  • Business Link High5ive, Pawia 9, 31-154 Kraków, Poland
  • Brain Embassy, Konstruktorska 11, 02-673 Warsaw, Poland
  • Aleja Grunwaldzka 472B, 80-309 Gdańsk, Poland

    The Codest

  • Home
  • About us
  • Services
  • Case studies
  • Know how
  • Careers

    Services

  • PHP development
  • Java development
  • Python development
  • Ruby on Rails development
  • React Developers
  • Vue Developers
  • TypeScript Developers
  • DevOps
  • QA Engineers

    Resources

  • What are top CTOs and CIOs Challenges? [2022 updated]
  • Facts and Myths about Cooperating with External Software Development Partner
  • From the USA to Europe: Why do American startups decide to relocate to Europe
  • Privacy policy
  • Website terms of use

Copyright © 2022 by The Codest. All rights reserved.

We use cookies on the site for marketing, analytical and statistical purposes. By continuing to use, without changing your privacy settings, our site, you consent to the storage of cookies in your browser. You can always change the cookie settings in your browser. You can find more information in our Privacy Policy.