fastapi-fsp
Filter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel.
fastapi-fsp helps you build standardized list endpoints that support:
- Filtering on arbitrary fields with rich operators (eq, ne, lt, lte, gt, gte, in, between, like/ilike, null checks, contains/starts_with/ends_with)
- Sorting by field (asc/desc)
- Pagination with page/per_page and convenient HATEOAS links
It is framework-friendly: you declare it as a FastAPI dependency and feed it a SQLModel/SQLAlchemy Select query and a Session.
Installation
Using uv (recommended):
# create & activate virtual env with uv
uv venv
. .venv/bin/activate
# add runtime dependency
uv add fastapi-fsp
Using pip:
pip install fastapi-fsp
Quick start
Below is a minimal example using FastAPI and SQLModel.
from typing import Optional
from fastapi import Depends, FastAPI
from sqlmodel import Field, SQLModel, Session, create_engine, select
from fastapi_fsp.fsp import FSPManager
from fastapi_fsp.models import PaginatedResponse
class HeroBase(SQLModel):
name: str = Field(index=True)
secret_name: str
age: Optional[int] = Field(default=None, index=True)
class Hero(HeroBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
class HeroPublic(HeroBase):
id: int
engine = create_engine("sqlite:///database.db", connect_args={"check_same_thread": False})
SQLModel.metadata.create_all(engine)
app = FastAPI()
def get_session():
with Session(engine) as session:
yield session
@app.get("/heroes/", response_model=PaginatedResponse[HeroPublic])
def read_heroes(*, session: Session = Depends(get_session), fsp: FSPManager = Depends(FSPManager)):
query = select(Hero)
return fsp.generate_response(query, session)
Run the app and query:
- Pagination:
GET /heroes/?page=1&per_page=10
- Sorting:
GET /heroes/?sort_by=name&order=asc
- Filtering:
GET /heroes/?field=age&operator=gte&value=21
The response includes data, meta (pagination, filters, sorting), and links (self, first, next, prev, last).
Query parameters
Pagination:
- page: integer (>=1), default 1
- per_page: integer (1..100), default 10
Sorting:
- sort_by: the field name, e.g.,
name
- order:
asc
or desc
Filtering (repeatable sets; arrays are supported by sending multiple parameters):
- field: the field/column name, e.g.,
name
- operator: one of
- eq, ne
- lt, lte, gt, gte
- in, not_in (comma-separated values)
- between (two comma-separated values)
- like, not_like
- ilike, not_ilike (if backend supports ILIKE)
- is_null, is_not_null
- contains, starts_with, ends_with (translated to LIKE patterns)
- value: raw string value (or list-like comma-separated depending on operator)
Examples:
?field=name&operator=eq&value=Deadpond
?field=age&operator=between&value=18,30
?field=name&operator=in&value=Deadpond,Rusty-Man
?field=name&operator=contains&value=man
You can chain multiple filters by repeating the triplet:
?field=age&operator=gte&value=18&field=name&operator=ilike&value=rust
Response model
{
"data": [ ... ],
"meta": {
"pagination": {
"total_items": 42,
"per_page": 10,
"current_page": 1,
"total_pages": 5
},
"filters": [
{"field": "name", "operator": "eq", "value": "Deadpond"}
],
"sort": {"sort_by": "name", "order": "asc"}
},
"links": {
"self": "/heroes/?page=1&per_page=10",
"first": "/heroes/?page=1&per_page=10",
"next": "/heroes/?page=2&per_page=10",
"prev": null,
"last": "/heroes/?page=5&per_page=10"
}
}
Development
This project uses uv as the package manager.
- Create env and sync deps:
uv venv
. .venv/bin/activate
uv sync --dev
- Run lint and format checks:
uv run ruff check .
uv run ruff format --check .
uv run pytest -q
uv build
CI/CD and Releases
GitHub Actions workflows are included:
- CI (lint + tests) runs on pushes and PRs.
- Release: pushing a tag matching
v*.*.*
runs tests, builds, and publishes to PyPI using PYPI_API_TOKEN
secret.
To release:
- Update the version in
pyproject.toml
.
- Push a tag, e.g.
git tag v0.1.1 && git push origin v0.1.1
.
- Ensure the repository has
PYPI_API_TOKEN
secret set (an API token from PyPI).
License
MIT License. See LICENSE.