Shiboru
DRF-style filtering for Rails APIs that feels like home to anyone coming from Django. If you’ve used DjangoFilter, this will click instantly.
Ransack exists and is great for many Rails apps, but for teams steeped in Django/DRF conventions the mental model never quite felt native. Shiboru was built to bring the DjangoFilter ergonomics to Rails: field__op=value params, association paths like user__email__icontains=..., first-class ordering and pagination, and per-model FilterSet classes with a sweet, explicit DSL.
Maintainers: Rishi Banerjee, Pratik Jain, Nikhil Anand, Dhruv Bhargava
Features
-
Per-model
FilterSetclasses:UserFilter,Profiles::UserFilter, inferred from class name. -
DRF/DjangoFilter-style operators:
__eq, __ne, __gt, __gte, __lt, __lte__contains, __icontains, __startswith, __istartswith, __endswith, __iendswith__in, __nin, __isnull, __range
-
Nested association paths:
profile__city__icontains=...,company__name__eq=.... -
Ordering:
?ordering=-created_at,name. -
Pagination:
page/page_sizeorlimit/offset. -
Whitelist DSL:
fields,related_fields,orderable_fields. -
Custom filters per resource:
filter :q { |scope, v| ... }. -
Postgres-friendly case-insensitive ops via
ILIKE. Optional pg_trgm migration for performance.
Installation
Add to your Rails app:
# Gemfile
gem "shiboru", git: "https://github.com/your-org/shiboru" # or path: "../shiboru"Install and run the installer:
bundle install
bin/spring stop
bin/rails g shiboru:install --pgtrgm # --pgtrgm is safe; no-ops on non-Postgres adapters
bin/rails db:migrateThe installer will create:
config/initializers/shiboru.rbapp/filters/.keep- Optionally a pg_trgm migration guarded to run only on Postgres
Quick start
Create a model:
bin/rails g model User name:string email:string:index age:integer active:boolean company:string signed_up_at:datetime
bin/rails db:migrateGenerate a filter:
bin/rails g shiboru:filter UserFilterEdit app/filters/user_filter.rb:
# frozen_string_literal: true
class UserFilter < Shiboru::FilterSet
# Model is inferred: User
fields :id, :name, :email, :age, :active, :company, :signed_up_at, :created_at, :updated_at
orderable_fields :name, :age, :company, :signed_up_at, :created_at
# Optional quick search (?q=...)
filter :q do |scope, value, context:|
v = "%#{value}%"
scope.where("users.name ILIKE ? OR users.email ILIKE ?", v, v)
end
endController and routes:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
include Shiboru::Controller
def index
render json: api_index(User, params) # returns {count,next,previous,results}
end
end# config/routes.rb
Rails.application.routes.draw do
resources :users, only: [:index]
endSeed a little data (optional):
# db/seeds.rb
%w[Acme Omnix Finlyt NovaLabs].each do |co|
5.times do |i|
User.create!(
name: ["Rishi Banerjee", "Pratik Jain", "Nikhil Anand", "Dhruv Bhargava"].sample + " #{i}",
email: "user#{i}+#{co.downcase}@example.com",
age: rand(18..60),
active: [true, false].sample,
company: co,
signed_up_at: rand(180).days.ago + rand(0..86_400)
)
end
endbin/rails db:seedTry it:
GET /users?name__icontains=rishi&age__gte=20&ordering=-created_at&page=1&page_size=10
GET /users?q=bhargava
The Filter DSL
Shiboru looks for a FilterSet class named after the model: UserFilter for User, Profiles::UserFilter for Profiles::User. Place filters in app/filters/**.
class ArticleFilter < Shiboru::FilterSet
fields :id, :title, :status, :views, :published_at, :category, :created_at
related_fields :user__name, :user__email, :user__company, :user__active
orderable_fields :published_at, :views, :created_at, :title
filter :q do |scope, value, context:|
v = "%#{value}%"
scope.where("articles.title ILIKE ? OR articles.body ILIKE ?", v, v)
end
endNotes:
-
fieldswhitelists filterable base-table columns. -
related_fieldswhitelists association fields viaassoc__fieldpaths. -
orderable_fieldswhitelists fields that can appear in?ordering=....
Query language
Any query param that matches path__operator=value becomes a filter.
Operators
- Equality and comparisons:
__eq,__ne,__gt,__gte,__lt,__lte - String matching:
__contains,__startswith,__endswith(case sensitive) - Case-insensitive variants (Postgres):
__icontains,__istartswith,__iendswith - Membership:
__in=1,2,3,__nin=4,5 - Null checks:
__isnull=true|false - Ranges:
__range=from,toorfrom..to
Examples:
/users?age__gte=25&age__lt=40
/users?email__icontains=example.com
/users?id__in=1,2,3
/users?signed_up_at__range=2025-01-01,2025-06-30
Associations
/articles?user__company__eq=Acme
/articles?user__active__eq=true&ordering=-published_at,title
Shiboru builds LEFT OUTER JOINs for association chains. If you filter a has_many chain on the “one” side and see duplicates, add distinct at the controller level or we can enable a built-in distinct_on_root toggle later.
Ordering
?ordering=-created_at,name
Multiple fields are comma-separated. Use - for descending. Association paths are supported:
/articles?ordering=-user__name,views
Pagination
Two modes, both supported:
- Page-based:
?page=2&page_size=50 - Offset-based:
?limit=50&offset=100
Response envelope matches DRF:
{
"count": 421,
"next": 3, // page number (or next offset in offset mode)
"previous": 1, // previous page (or previous offset)
"results": [ ... ]
}ENV["API_MAX_LIMIT"] caps maximum page size and limit (default 100).
Controllers
Include the helper and call api_index(Model, params):
class ArticlesController < ApplicationController
include Shiboru::Controller
def index
render json: api_index(Article, params)
end
endOptionally add a serializer:
render json: api_index(Article, params, serializer: ArticleSerializer)Generators
Install:
bin/rails g shiboru:install --pgtrgmCreate a filter:
bin/rails g shiboru:filter UserFilter
bin/rails g shiboru:filter Articles::PostFilter # for Articles::Post modelTemplates live in lib/generators/shiboru/... inside the gem.
Performance
-
Add btree indexes on equality/ordering fields (
created_at, foreign keys, etc.). -
For
__icontainsand other case-insensitive matchers on Postgres:-
enable pg_trgm (
--pgtrgmin installer), -
create trigram indexes:
CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX CONCURRENTLY idx_users_name_trgm ON users USING gin (name gin_trgm_ops); CREATE INDEX CONCURRENTLY idx_users_email_trgm ON users USING gin (email gin_trgm_ops); CREATE INDEX CONCURRENTLY idx_articles_title_trgm ON articles USING gin (title gin_trgm_ops);
-
Security
- The DSL is a whitelist. Only fields you declare are filterable/orderable.
- If
related_fieldsis omitted, Shiboru allows all columns on joined targets. For strict security, always declarerelated_fieldsexplicitly.
Example: Users and Articles
Models:
class User < ApplicationRecord
has_many :articles, dependent: :destroy
end
class Article < ApplicationRecord
belongs_to :user
endFilters:
class UserFilter < Shiboru::FilterSet
fields :id, :name, :email, :age, :active, :company, :signed_up_at, :created_at
orderable_fields :name, :age, :company, :signed_up_at, :created_at
filter :q do |scope, v, context:|
like = "%#{v}%"
scope.where("users.name ILIKE ? OR users.email ILIKE ?", like, like)
end
end
class ArticleFilter < Shiboru::FilterSet
fields :id, :title, :status, :views, :published_at, :category, :created_at
related_fields :user__name, :user__email, :user__company, :user__active
orderable_fields :published_at, :views, :title, :created_at
filter :q do |scope, v, context:|
like = "%#{v}%"
scope.where("articles.title ILIKE ? OR articles.body ILIKE ?", like, like)
end
endRequests:
/users?age__gte=25&company__eq=Acme&ordering=-signed_up_at
/users?q=Anand&page=1&page_size=20
/articles?status__in=published,archived&user__company__eq=Acme
/articles?views__gte=1000&ordering=-views,title&limit=30&offset=60
How it works
-
Shiboru::RegistrymapsModel→ModelFilter(namespaced aware). -
FilterSet:- Parses params into filter instructions (
path,op,value). - Validates against whitelists.
- Builds
LEFT OUTER JOINs for association paths. - Applies
WHEREbased on operators with bind parameters. - Applies
ORDER BYand pagination. - Returns a DRF-like envelope.
- Parses params into filter instructions (
Why not just use Ransack?
Ransack is powerful, battle-tested, and a fine choice for many Rails apps. Teams coming from Django/DRF often prefer the field__op=value grammar and the mental model of FilterSet classes that mirror the resource. Shiboru embraces that exact style so Rails APIs can feel like DRF without translation overhead.
Reference: DjangoFilter documentation https://django-filter.readthedocs.io/
Development
- Ruby 3.1+
- Rails 6.1+ (tested on 7/8)
Run tests:
bundle exec rspecLicense
MIT