FastAPI QueryBuilder
Powerful, flexible query building for FastAPI and SQLAlchemy. Add advanced filtering, sorting, and searching to your API endpoints with minimal code.
Installation
Install FastAPI QueryBuilder using pip. It's that simple!
pip install fastapi-querybuilder
Requirements
- Python 3.10+
- FastAPI 0.115+
- SQLAlchemy 2.0+
- fastapi-pagination 0.13.2+
Quick Start
Get up and running in less than a minute with this simple example.
from fastapi import FastAPI, Depends
from fastapi_querybuilder import QueryBuilder
from sqlalchemy.ext.asyncio import AsyncSession
app = FastAPI()
@app.get("/users")
async def get_users(
query=QueryBuilder(User),
session: AsyncSession = Depends(get_db)
):
result = await session.execute(query)
return result.scalars().all()
That's it!
Your endpoint now supports filtering, sorting, and searching through query parameters.
Filtering
Advanced JSON-based filters
?filters={"name": {"$eq": "John"}}
Sorting
Multi-field sorting with nested paths
?sort=status:asc,created_at:desc
Searching
Smart search with explicit fields
?search=john
Core Features
Advanced Filtering
Build complex queries with JSON-based filters and multiple operators.
Case Sensitivity
String filters using $eq, $ne, and $in are case-insensitive by default.
Set case_sensitive=true to use legacy case-sensitive string filtering.
For enum columns (for example PostgreSQL enums), case-insensitive matching is enum-safe and uses text casting internally.
Simple Filter
{"name": {"$eq": "John Doe"}}
Multiple Conditions
{
"$and": [
{"age": {"$gte": 18}},
{"is_active": {"$eq": true}},
{"status": {"$in": ["active", "pending"]}}
]
}
Logical OR
{
"$or": [
{"name": {"$contains": "john"}},
{"email": {"$contains": "john"}}
]
}
Dynamic Sorting
Sort results by any field, including nested relationships. Supports multiple sort clauses and both dot and double-underscore notation.
Case-Insensitive Text Sorting
String sorting is case-insensitive by default. Use case_sensitive=true to restore legacy case-sensitive text ordering.
Enum sorting is also safe in case-insensitive mode and enum fields are never treated as timestamp-like strings based on field name suffixes.
?sort=name:asc
?sort=created_at:desc
?sort=status:asc,created_at:desc
?sort=role.name:asc
?sort=role__name:asc
?sort=role.department.name:desc
?sort=name:asc&case_sensitive=true
Multi-Sort Details
- Left to Right: Sort clauses are applied in order (left to right)
- Flexible Syntax: Use dot notation (role.name) or double-underscore (role__name)
- Default Behavior: String sorting is case-insensitive unless
case_sensitive=true - Enum Safety: Enum fields are cast safely for case-insensitive sorting and are excluded from timestamp-like date casting rules
- Date-Aware: Timestamp-like string fields (created_at/updated_at) are sorted chronologically
- Validated: Invalid directions return clear 400 error messages
- Smart Joins: Automatically creates necessary joins for nested relationships
Smart Search with Field Control
Powerful search with explicit field control for optimal performance and predictable results.
Performance-Optimized Search
By default, search only queries the root model's fields (5-100x faster). Use search_fields parameter to explicitly search nested relationships.
# Default: Search only User model fields (fast, no joins)
GET /users?search=john
# Explicit fields: Search specific top-level fields
GET /users?search=john&search_fields=name,email
# Nested search: Search in related models
GET /users?search=admin&search_fields=name,role.name
# Deep nesting: Multiple relationship levels
GET /users?search=Engineering&search_fields=role.department.name
# Mixed: Top-level + nested fields
GET /users?search=dev&search_fields=name,email,role.name,role.department.name
Search Field Types
String Fields
Case-insensitive ILIKE search
Enum Fields
Matches enum values
Integer Fields
Exact numeric match
Boolean Fields
True/false matching
Key Benefits
- Fast Default: Top-level search = 0 joins, no DISTINCT overhead
- Explicit Control: Specify exactly which fields to search
- Nested Paths: Use dot notation for relationships (role.name, role.department.name)
- Safe: Circular reference detection prevents infinite loops
- Smart Joins: Creates joins only for specified nested paths
Pagination Support
Seamless integration with fastapi-pagination for easy result pagination.
from fastapi_pagination import Page, add_pagination
from fastapi_pagination.ext.sqlalchemy import paginate
@app.get("/users/paginated", response_model=Page[UserResponse])
async def get_users_paginated(
query=QueryBuilder(User),
session: AsyncSession = Depends(get_db)
):
return await paginate(session, query)
add_pagination(app)
API Reference
Comparison Operators
$eq
EqualMatches values equal to specified value (case-insensitive for strings by default)
{"age": {"$eq": 25}}
LOWER(name) = 'john' or LOWER(CAST(status AS VARCHAR)) = 'active' or age = 25$ne
Not EqualMatches values not equal to specified value (case-insensitive for strings by default)
{"status": {"$ne": "inactive"}}
LOWER(name) != 'inactive' or LOWER(CAST(status AS VARCHAR)) != 'inactive'$gt
Greater ThanMatches values greater than specified value
{"age": {"$gt": 18}}
age > 18$gte
Greater/EqualMatches values greater than or equal to
{"age": {"$gte": 21}}
age >= 21$lt
Less ThanMatches values less than specified value
{"age": {"$lt": 65}}
age < 65$lte
Less/EqualMatches values less than or equal to
{"age": {"$lte": 64}}
age <= 64$in
In ArrayMatches any value in an array (case-insensitive for string arrays by default)
{"status": {"$in": ["active", "pending"]}}
LOWER(name) IN ('active', 'pending') or LOWER(CAST(status AS VARCHAR)) IN ('active', 'pending')$isanyof
Alias: $inAlternative syntax for $in operator
{"role": {"$isanyof": ["admin", "user"]}}
role IN (...)String Operators
$contains
ContainsCase-insensitive substring match
{"name": {"$contains": "john"}}
name ILIKE '%john%' or CAST(status AS VARCHAR) ILIKE '%john%'$ncontains
Not ContainsDoes not contain substring
{"name": {"$ncontains": "test"}}
name NOT ILIKE '%test%' or CAST(status AS VARCHAR) NOT ILIKE '%test%'$startswith
Starts WithMatches strings starting with value
{"email": {"$startswith": "admin"}}
email ILIKE 'admin%' or CAST(status AS VARCHAR) ILIKE 'admin%'$endswith
Ends WithMatches strings ending with value
{"email": {"$endswith": ".com"}}
email ILIKE '%.com' or CAST(status AS VARCHAR) ILIKE '%.com'Null/Empty Operators
$isempty
Is NullMatches null/empty values
{"description": {"$isempty": true}}
description IS NULL$isnotempty
Not NullMatches non-null values
{"description": {"$isnotempty": true}}
description IS NOT NULLLogical Operators
$and
ANDAll conditions must be true
{"$and": [{"age": {"$gte": 18}}, {"is_active": true}]}
age >= 18 AND is_active = true$or
ORAt least one condition must be true
{"$or": [{"name": {"$contains": "john"}}, {"email": {"$contains": "john"}}]}
name ILIKE '%john%' OR email ILIKE '%john%'
Advanced Features
Nested Relationships
Query and filter through nested relationships using dot notation.
{
"role.name": {"$eq": "admin"},
"role.department.name": {"$contains": "Engineering"}
}
Tip
You can nest relationships to any depth. QueryBuilder automatically creates the necessary joins.
Soft Delete Support
Automatically excludes soft-deleted records when your model has a deleted_at field.
class User(Base):
__tablename__ = "users"
# ... other fields ...
deleted_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
# QueryBuilder automatically adds: WHERE deleted_at IS NULL
Smart Date Handling
Automatic date range processing for date-only strings.
{"created_at": {"$eq": "2023-12-01"}}
Matches entire day (00:00:00 to 23:59:59)
{"created_at": {"$eq": "2023-12-01T10:30:00"}}
Exact timestamp match
Supported Formats
YYYY-MM-DD- Date onlyYYYY-MM-DDTHH:MM:SS- ISO formatYYYY-MM-DD HH:MM:SS- Space separatedYYYY-MM-DDTHH:MM:SSZ- UTC timezone
Real-World Examples
Find active admin users in Engineering
GET /users?filters={
"is_active": {"$eq": true},
"role.name": {"$eq": "admin"},
"role.department.name": {"$eq": "Engineering"}
}
Complex query with multiple conditions
GET /users?filters={
"$and": [
{
"$or": [
{"name": {"$contains": "john"}},
{"email": {"$contains": "john"}}
]
},
{"age": {"$gte": 18}},
{"role.name": {"$in": ["admin", "user"]}}
]
}&sort=created_at:desc
Date range with sorting and search
GET /users?filters={
"created_at": {"$gte": "2023-01-01", "$lt": "2024-01-01"}
}&search=engineer&sort=role.name:asc
Paginated results with filters
GET /users/paginated?page=1&size=20&filters={
"age": {"$gte": 25},
"status": {"$in": ["active", "pending"]}
}&sort=name:asc