DEV Community

chrisfrank
chrisfrank

Posted on • Edited on

Dynamically filter data via URL params with Rack::Reducer

Whether you're building a tiny API or a giant monolith, your app probably needs to render a list of database records somewhere. And you probably want those records to be filterable via URL params, like so:

GET /artists?genre=electronic&sort=name

It'd be nice to sort or filter when the relevant params are present, and return a sensible default dataset otherwise.

GET /artists?name=blake` => artists named 'blake'
GET /artists?genre=electronic&sort=name => electronic artists, sorted by name
GET /artists => all artists
Enter fullscreen mode Exit fullscreen mode

You could conditionally apply filters with hand-written if statements, but that approach gets uglier the more filters you add.

In Rails, you could use Plataformatec's venerable HasScope gem.

But what if you're working in Roda, Sinatra, or Hanami? Even in Rails, what if you'd rather write dedicated query objects than pollute your models and controllers with filtering code?

Rack::Reducer can help. It's a gem that maps incoming URL params to an array of filter functions you define, applies only the applicable filters, and returns your filtered data. It can make your controller logic as minimal as...

@artists = Artist.reduce(params)
Enter fullscreen mode Exit fullscreen mode

...or, if magical, implicit code isn't your thing, you can call Rack::Reducer as a function to build more explicit queries -- maybe right in your controllers, maybe in dedicated query objects, or really anywhere you like.

# app/controllers/artists_controller.rb
class ArtistsController < ApplicationController
  def index
    @artists = Rack::Reducer.call(params, dataset: Artist.all, filters: [
      ->(name:) { where('lower(name) like ?', "%#{name.downcase}%") },
      ->(genre:) { where(genre: genre) },
      ->(sort:) { order(sort.to_sym) },
    ])
    render json: @artists
  end
end
Enter fullscreen mode Exit fullscreen mode

Rack::Reducer works in any Rack-compatible app, with any ORM, and has no dependencies beyond Rack itself. Full documentation is on GitHub. Whether you work in Rails, Sinatra, Roda, Hanami, or raw Rack, I hope it can be of use to you.

Top comments (8)

Collapse
 
dmerand profile image
Donald Merand

Very interesting idea! Might I ask what use case brought you to that solution?

Collapse
 
chrisfrank profile image
chrisfrank

I've been working mostly in Roda and Sinatra this year and less in Rails. One of the things I've missed from Rails is Platformatec's HasScope gem, which solves this filtering problem well. So I set out to write a library that would work in any Rack app.

Turns out I like Rack::Reducer's API better than HasScope's, even in Rails. That's not meant as a dig at HasScope—Reducer relies on features of ruby 2.1+, which didn't exist when HasScope was written.

Collapse
 
dmerand profile image
Donald Merand

Interesting - I hadn't seen Roda before, but I'm guessing that you're trending away from "batteries included" frameworks :-)

Do you find yourself using Sinatra and/or Roda on bigger projects? Are you doing a lot of ORM in those environments?

Thread Thread
 
chrisfrank profile image
chrisfrank

I like Sinatra for tiny projects, and I really love Roda (with Sequel as an ORM) for large ones.

Roda’s concept of a “routing tree” is very similar, structurally, to the way react-router manages client-side routing. So when I build an API in Roda and a UI in React, I can reason about them the same way, despite their being in different languages. In general, I think I value structural similarity more than syntactical similarity — which is part of why I still build APIs in ruby/roda, instead of js/express.

Thread Thread
 
dmerand profile image
Donald Merand

This is all very cool! I might try a project using Roda+Sequel, just to get a feel for some Rails ORM alternatives. I use Sinatra quite a bit, but tend to kick over to Rails if I need heavy DB work.

I like your point about structural similarity - I think this same logic is why folks are loving the Elixir/Phoenix + Elm backend/frontend combination. I'm headed in that direction for my larger projects (though I still love Ruby).

Collapse
 
katafrakt profile image
Paweł Świątkowski

This post has bean featured in Issue #11 of Ruby Tuesday: rubytuesday.katafrakt.me/issues/20...

Collapse
 
chrisfrank profile image
chrisfrank

Neat, thank you! I look forward to the next issue.

Collapse
 
mmferreira2000 profile image
Matheus Ferreira

Awsome! Does it work with Rails 7?