47 Commits

Author SHA1 Message Date
a3ccea2e91 chore(deps): update nginx docker tag to v1.30 2026-04-16 00:03:01 +00:00
a37cafc53c update fastapi pagination article
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m55s
2026-04-09 03:59:56 -04:00
434306d9f1 update fastapi pagination article
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m19s
2026-03-22 18:38:01 -04:00
d2b1548dd3 add fastapi pagination article
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 2m45s
2026-03-21 06:53:29 -04:00
106df3574c update fastapi-toolsets project
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 2m50s
2026-03-01 10:05:04 -05:00
4512844027 bump: blowfish/hugo version
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 2m24s
2026-03-01 09:55:24 -05:00
24f78bcd31 add fastapi-toolsets project
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 2m4s
2026-02-09 15:57:02 -05:00
711240311c bump: blowfish/hugo version + change images extension 2026-02-09 15:56:19 -05:00
c375fd99fa bump copyright to 2026
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 4m54s
2026-01-04 10:22:05 +01:00
b278ffaf95 bump: blowfish/hugo version
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 2m21s
2025-12-20 11:00:13 +01:00
7236052a4a bump: blowfish/hugo version
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m46s
2025-08-14 17:04:33 +02:00
dc0a2e036f bump: blowfish/hugo version
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m22s
2025-07-11 05:44:23 -04:00
78bd487cfe bump: blowfish/hugo version + theme submodule
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m45s
2025-06-28 07:42:39 -04:00
f342bf94ad Merge pull request 'chore(deps): update nginx docker tag to v1.29' (#12) from renovate/nginx-1.x into main
Some checks failed
Build Blog Docker Image / build docker (push) Has been cancelled
Reviewed-on: #12
2025-06-27 20:10:14 +02:00
367670897b chore(deps): update nginx docker tag to v1.29 2025-06-25 00:00:40 +00:00
030690546d Revert "bump: blowfish/hugo version"
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m18s
This reverts commit 834fc3f045.
2025-05-03 05:31:31 -04:00
834fc3f045 bump: blowfish/hugo version
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m26s
2025-05-03 05:09:11 -04:00
0e7b6a4634 chore(build): use hugomods/hugo base image for build
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m32s
2025-04-26 05:49:15 -04:00
295da91a77 Merge pull request 'chore(deps): update nginx docker tag to v1.28' (#10) from renovate/nginx-1.x into main
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 2m19s
Reviewed-on: #10
2025-04-26 11:37:52 +02:00
8ab461d601 chore(deps): update nginx docker tag to v1.28 2025-04-24 00:00:40 +00:00
616accd48f bump: blowfish/hugo version
All checks were successful
Build Hugo Docker Image / build docker (push) Successful in 6m24s
Build Blog Docker Image / build docker (push) Successful in 3m15s
2025-03-30 15:07:49 -04:00
24926644b6 Merge pull request 'chore(deps): update golang docker tag to v1.24' (#9) from renovate/golang-1.x into main
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m41s
Reviewed-on: #9
2025-02-13 23:05:47 +01:00
4396c6b534 chore(deps): update golang docker tag to v1.24 2025-02-13 00:01:20 +00:00
957439daf7 bump: blowfish/hugo version
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 4m5s
Build Hugo Docker Image / build docker (push) Successful in 5m43s
2025-02-03 12:56:45 -05:00
1dbdec4125 bump: blowfish/hugo version
All checks were successful
Build Hugo Docker Image / build docker (push) Successful in 4m3s
Build Blog Docker Image / build docker (push) Successful in 1m4s
2025-01-15 14:37:32 -05:00
7540750b10 update umami websiteid
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m17s
2025-01-12 18:07:40 -05:00
b024458812 revert ec19ee3b15
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m13s
revert fix(Dockerfile): remove exposed port
2025-01-09 20:10:51 +01:00
ec19ee3b15 fix(Dockerfile): remove exposed port
Some checks failed
Build Blog Docker Image / build docker (push) Failing after 1m24s
2025-01-08 22:20:03 +01:00
0ffb63994c update copyright to 2025
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m3s
2025-01-04 05:49:10 -05:00
90f8f2077d fix: profile data
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m4s
2024-12-21 05:21:24 -05:00
b29787f86f fix: paginate deprecation
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m1s
2024-12-11 16:17:59 +01:00
45c231a7a1 bump: blowfish/hugo version
All checks were successful
Build Hugo Docker Image / build docker (push) Successful in 4m30s
Build Blog Docker Image / build docker (push) Successful in 1m10s
2024-12-11 16:08:59 +01:00
29a6bd78ff fix: .Site.Author deprecation
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m16s
2024-11-11 18:10:03 +01:00
8ad53f40ff bump: blowfish/hugo version
Some checks failed
Build Hugo Docker Image / build docker (push) Successful in 4m9s
Build Blog Docker Image / build docker (push) Failing after 1m8s
2024-11-10 19:47:44 +01:00
39d14e056f bump: blowfish/hugo version
All checks were successful
Build Hugo Docker Image / build docker (push) Successful in 5m55s
Build Blog Docker Image / build docker (push) Successful in 1m17s
2024-10-07 21:41:49 +02:00
db9ebf501c reverte 005a77e8b1
All checks were successful
Build Hugo Docker Image / build docker (push) Successful in 4m40s
Build Blog Docker Image / build docker (push) Successful in 1m19s
2024-09-23 18:41:02 +02:00
005a77e8b1 fix: change runners
Some checks failed
Build Hugo Docker Image / build docker (push) Failing after 1m10s
Build Blog Docker Image / build docker (push) Successful in 1m54s
2024-09-23 18:38:56 +02:00
a8a307d6bd Merge pull request 'chore(deps): update docker/login-action action to v3' (#6) from renovate/docker-login-action-3.x into main
Some checks are pending
Build Hugo Docker Image / build docker (push) Waiting to run
Build Blog Docker Image / build docker (push) Waiting to run
Reviewed-on: #6
2024-09-23 18:21:52 +02:00
bdc1af6115 Merge pull request 'chore(deps): update actions/checkout action to v4' (#4) from renovate/actions-checkout-4.x into main
Some checks are pending
Build Hugo Docker Image / build docker (push) Waiting to run
Build Blog Docker Image / build docker (push) Waiting to run
Reviewed-on: #4
2024-09-23 18:21:44 +02:00
a79d0de3d7 Merge pull request 'chore(deps): update docker/build-push-action action to v6' (#5) from renovate/docker-build-push-action-6.x into main
Some checks are pending
Build Hugo Docker Image / build docker (push) Waiting to run
Build Blog Docker Image / build docker (push) Waiting to run
Reviewed-on: #5
2024-09-23 18:21:32 +02:00
c7c2a43f50 Merge pull request 'chore(deps): update docker/setup-buildx-action action to v3' (#7) from renovate/docker-setup-buildx-action-3.x into main
Some checks are pending
Build Hugo Docker Image / build docker (push) Waiting to run
Build Blog Docker Image / build docker (push) Waiting to run
Reviewed-on: #7
2024-09-23 18:21:16 +02:00
78d009d6e0 Merge pull request 'chore(deps): update docker/setup-qemu-action action to v3' (#8) from renovate/docker-setup-qemu-action-3.x into main
Some checks are pending
Build Hugo Docker Image / build docker (push) Waiting to run
Build Blog Docker Image / build docker (push) Waiting to run
Reviewed-on: #8
2024-09-23 18:21:10 +02:00
db64b78ab9 chore(deps): update docker/setup-qemu-action action to v3
All checks were successful
Build Hugo Docker Image / build docker (push) Successful in 5m4s
2024-09-23 16:20:30 +00:00
79f0c18538 chore(deps): update docker/setup-buildx-action action to v3
All checks were successful
Build Hugo Docker Image / build docker (push) Successful in 4m50s
2024-09-23 16:20:28 +00:00
771a04a6a7 chore(deps): update docker/login-action action to v3
All checks were successful
Build Hugo Docker Image / build docker (push) Successful in 4m58s
2024-09-23 16:20:26 +00:00
5f9bc01b8b chore(deps): update docker/build-push-action action to v6
All checks were successful
Build Hugo Docker Image / build docker (push) Successful in 4m55s
2024-09-23 16:20:24 +00:00
bac32586b8 chore(deps): update actions/checkout action to v4
All checks were successful
Build Hugo Docker Image / build docker (push) Successful in 4m42s
2024-09-23 16:20:21 +00:00
36 changed files with 564 additions and 73 deletions

View File

@@ -1,33 +0,0 @@
name: Build Hugo Docker Image
on:
push:
paths:
- ".gitea/workflows/1_build_hugo_image.yml"
jobs:
build docker:
runs-on: linux_amd
steps:
- name: checkout code
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker registry
uses: docker/login-action@v2
with:
registry: git.d3vyce.fr
username: ${{ github.actor }}
password: ${{ secrets.GIT_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
build-args:
HUGO_VERSION=v0.133.1
file: ./hugo.Dockerfile
platforms: linux/amd64
push: true
tags: git.d3vyce.fr/d3vyce/hugo:latest

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: linux_amd runs-on: linux_amd
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
# with: # with:
# lfs: 'true' # lfs: 'true'
- name: Checkout LFS - name: Checkout LFS
@@ -21,21 +21,19 @@ jobs:
/usr/bin/git lfs fetch origin refs/remotes/origin/${{ gitea.ref_name }} /usr/bin/git lfs fetch origin refs/remotes/origin/${{ gitea.ref_name }}
/usr/bin/git lfs checkout /usr/bin/git lfs checkout
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to Docker registry - name: Login to Docker registry
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: git.d3vyce.fr registry: git.d3vyce.fr
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GIT_TOKEN }} password: ${{ secrets.GIT_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v6
with: with:
context: . context: .
build-args:
BLOWFISH_VERSION=v2.77.1
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64 platforms: linux/amd64
push: true push: true

View File

@@ -1,5 +1,5 @@
# Build Stage # Build Stage
FROM git.d3vyce.fr/d3vyce/hugo:latest AS build FROM hugomods/hugo:0.155.3 AS build
ARG BLOWFISH_VERSION ARG BLOWFISH_VERSION
@@ -7,11 +7,11 @@ WORKDIR /opt/blog
COPY . /opt/blog/ COPY . /opt/blog/
RUN git submodule update --init --recursive && \ RUN git submodule update --init --recursive && \
git -C themes/blowfish/ checkout ${BLOWFISH_VERSION} git -C themes/blowfish/ checkout v2.98.0
RUN hugo RUN hugo
# Publish Stage # Publish Stage
FROM nginx:1.27-alpine FROM nginx:1.30-alpine
WORKDIR /usr/share/nginx/html WORKDIR /usr/share/nginx/html
COPY --from=build /opt/blog/public /usr/share/nginx/html/ COPY --from=build /opt/blog/public /usr/share/nginx/html/

View File

@@ -9,7 +9,6 @@ defaultContentLanguage = "en"
# pluralizeListTitles = "true" # hugo function useful for non-english languages, find out more in https://gohugo.io/getting-started/configuration/#pluralizelisttitles # pluralizeListTitles = "true" # hugo function useful for non-english languages, find out more in https://gohugo.io/getting-started/configuration/#pluralizelisttitles
enableRobotsTXT = true enableRobotsTXT = true
paginate = 10
summaryLength = 0 summaryLength = 0
buildDrafts = false buildDrafts = false
@@ -17,6 +16,9 @@ buildFuture = false
# googleAnalytics = "G-XXXXXXXXX" # googleAnalytics = "G-XXXXXXXXX"
[pagination]
pagerSize = 10
[imaging] [imaging]
anchor = 'Center' anchor = 'Center'

View File

@@ -11,9 +11,9 @@ title = "d3vyce Blog"
logo = "img/author_transparent.webp" logo = "img/author_transparent.webp"
# secondaryLogo = "img/secondary-logo.png" # secondaryLogo = "img/secondary-logo.png"
description = "Hi 👋, Welcome to my Blog!" description = "Hi 👋, Welcome to my Blog!"
copyright = "d3vyce 2024 © All rights reserved." copyright = "d3vyce 2021-2026 © All rights reserved."
[author] [params.author]
name = "d3vyce" name = "d3vyce"
image = "img/profil.png" image = "img/profil.png"
headline = "Hi 👋, Welcome to my Blog!" headline = "Hi 👋, Welcome to my Blog!"

View File

@@ -143,5 +143,5 @@ smartTOCHideUnfocusedChildren = false
# yandex = "" # yandex = ""
[umamiAnalytics] [umamiAnalytics]
websiteid = "3f82b8d1-0744-4af3-a29e-c1b5add4a1e5" websiteid = "136df6e7-f469-45ce-b2c1-f324e7228ea5"
domain = "analytics.d3vyce.fr" domain = "analytics.d3vyce.fr"

View File

@@ -1,7 +1,6 @@
--- ---
title: "d3vyce Blog's" title: "d3vyce Blog's"
description: "This page was built using the Blowfish theme for Hugo." description: "This page was built using the Blowfish theme for Hugo."
layout: "simple"
--- ---
{{< list title="Latest Projects" cardView=true limit=3 where="Type" value="projects" >}} {{< list title="Latest Projects" cardView=true limit=3 where="Type" value="projects" >}}

View File

@@ -0,0 +1,434 @@
---
title: "How to implement pagination, sorting and filtering with FastAPI ?"
date: 2026-03-21
slug: "how-to-implement-pagination-sorting-and-filtering-with-fastapi"
tags: ["python", "fastapi"]
type: "programming"
---
## Overview
Pagination, filtering, and sorting are features you end up implementing in almost every API. This ensures good performance and a modern user experience, which is what most websites use.
In this article, I'll walk through how I handle these concerns in my FastAPI projects using [fastapi-toolsets](https://github.com/d3vyce/fastapi-toolsets), a small library I built around SQLAlchemy async and Postgres.
The example we'll use is a simple article listing API with offset and cursor pagination, full-text search, facet filtering, and client-driven sorting.
## Offset vs cursor pagination
Before diving into the implementation, it's worth understanding the two pagination strategies and when to use each.
### Offset pagination
Offset pagination is the classic approach. You pass a page number and a page size, and the database skips `(page - 1) * size` rows before returning results.
**Standard REST API convention:**
```
GET /articles?page=2&items_per_page=20
```
**Typical response shape:**
```json
{
"items": [...],
"pagination": {
"total_count": 143,
"items_per_page": 20,
"page": 2,
"has_more": true,
"pages": 8
}
}
```
It's simple, predictable, and lets clients jump to any page. The downside is performance: `OFFSET` forces the database to scan and discard all preceding rows. On large tables with high offsets, this gets slow. There's also a consistency issue — if a new item is inserted while the client is paginating, pages can shift and items appear duplicated or skipped.
### Cursor pagination
Cursor pagination replaces the page number with an opaque token representing the position of the last seen item. The server uses this cursor to seek directly to the next page.
**Standard REST API convention:**
```
GET /articles?cursor=eyJpZCI6IjEyMyJ9&items_per_page=20
```
**Typical response shape:**
```json
{
"items": [...],
"pagination": {
"next_cursor": "eyJjcmVhdGVkX2F0IjogIjIwMjYtMDMtMTBUMDg6MTQ6MDBaIn0=",
"prev_cursor": null,
"items_per_page": 20,
"has_more": true
}
}
```
Performance stays constant regardless of how deep into the dataset you are, and the result set is stable even if rows are inserted concurrently. The trade-off: you lose the ability to jump to an arbitrary page, which makes it less suited for traditional paginated tables.
### When to use which
| | Offset | Cursor |
|---|---|---|
| Jump to arbitrary page | Yes | No |
| Performance on large datasets | Degrades with high offsets | Stable |
| Stable results under concurrent writes | No | Yes |
| Best for | Admin tables, search results | Feeds, infinite scroll, large datasets |
## Project structure
The example is organized as a standard FastAPI app:
```
pagination_search/
├── app.py # FastAPI app setup
├── db.py # SQLAlchemy async engine + session dependency
├── models.py # SQLAlchemy models
├── crud.py # CrudFactory declaration
├── schemas.py # Pydantic response schemas
└── routes.py # API routes
```
### Models
We have two models: `Category` and `Article`. The `Article` model uses the [CreatedAtMixin](https://fastapi-toolsets.d3vyce.fr/module/models/#createdatmixin) provided by `fastapi-toolsets` to automatically add a `created_at` timestamp — which we'll use later as the cursor column for cursor-based pagination.
```python
import uuid
from sqlalchemy import Boolean, ForeignKey, String, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from fastapi_toolsets.models import CreatedAtMixin
class Base(DeclarativeBase):
pass
class Category(Base):
__tablename__ = "categories"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(64), unique=True)
articles: Mapped[list["Article"]] = relationship(back_populates="category")
class Article(Base, CreatedAtMixin):
__tablename__ = "articles"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
title: Mapped[str] = mapped_column(String(256))
body: Mapped[str] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(32))
published: Mapped[bool] = mapped_column(Boolean, default=False)
category_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("categories.id"), nullable=True
)
category: Mapped["Category | None"] = relationship(back_populates="articles")
```
### Database setup
The database layer uses SQLAlchemy's async engine with `asyncpg`. `fastapi-toolsets` provides two helpers: [create_db_dependency](https://fastapi-toolsets.d3vyce.fr/module/db/#session-dependency) for use as a FastAPI `Depends`, and [create_db_context](https://fastapi-toolsets.d3vyce.fr/module/db/#session-context-manager) for use as an async context manager outside of request handling.
```python
from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from fastapi_toolsets.db import create_db_context, create_db_dependency
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/postgres"
engine = create_async_engine(url=DATABASE_URL, future=True)
async_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False)
get_db = create_db_dependency(session_maker=async_session_maker)
get_db_context = create_db_context(session_maker=async_session_maker)
SessionDep = Annotated[AsyncSession, Depends(get_db)]
```
### Declaring the CRUD factory
This is where the magic happens. Instead of writing query logic in every route, [CrudFactory](https://fastapi-toolsets.d3vyce.fr/module/crud/#creating-a-crud-class) centralizes all the configuration in one place: which fields are searchable, which can be used as facet filters, and which are exposed for client-driven ordering.
```python
from fastapi_toolsets.crud import CrudFactory
from .models import Article, Category
ArticleCrud = CrudFactory(
model=Article,
cursor_column=Article.created_at, # column used for cursor pagination
searchable_fields=[ # fields included in full-text search
Article.title,
Article.body,
(Article.category, Category.name), # joined relation field
],
facet_fields=[ # fields exposed as filter dropdowns
Article.status,
(Article.category, Category.name),
],
order_fields=[ # fields the client can sort by
Article.title,
Article.created_at,
],
)
```
> The tuple syntax `(Article.category, Category.name)` tells the factory to join the `Category` table and expose `Category.name` as a searchable/filterable field — no manual join needed in routes.
### Response schema
A minimal Pydantic schema for the article list response:
```python
import datetime
import uuid
from fastapi_toolsets.schemas import PydanticBase
class ArticleRead(PydanticBase):
id: uuid.UUID
created_at: datetime.datetime
title: str
status: str
published: bool
category_id: uuid.UUID | None
```
### Routes
With the CRUD factory declared, routes become thin wrappers. Each route uses [ArticleCrud.filter_params()](https://fastapi-toolsets.d3vyce.fr/module/crud/#faceted-search) and [ArticleCrud.order_params()](https://fastapi-toolsets.d3vyce.fr/module/crud/#sorting) as FastAPI dependencies — these automatically generate the right query parameters based on the `facet_fields` and `order_fields` we declared.
#### Offset pagination
```python
@router.get("/offset")
async def list_articles_offset(
session: SessionDep,
params: Annotated[
dict,
Depends(
ArticleCrud.offset_paginate_params(
default_page_size=20,
max_page_size=100,
default_order_field=Article.created_at,
)
),
],
) -> OffsetPaginatedResponse[ArticleRead]:
return await ArticleCrud.offset_paginate(
session=session,
**params,
schema=ArticleRead,
)
```
![Offset endpoint](img/image-1.webp)
**Example request:**
```
GET /articles/offset?page=2&items_per_page=2&search=fastapi&status=published&order_by=created_at&order_dir=desc
```
**Example response:**
```json
{
"items": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"created_at": "2026-03-15T10:22:00Z",
"title": "Getting started with FastAPI",
"status": "published",
"published": true,
"category_id": "f1e2d3c4-b5a6-7890-abcd-ef0987654321"
},
{
"id": "b2c3d4e5-f6a7-8901-bcde-f01234567891",
"created_at": "2026-03-10T08:14:00Z",
"title": "FastAPI dependency injection explained",
"status": "published",
"published": true,
"category_id": "f1e2d3c4-b5a6-7890-abcd-ef0987654321"
}
],
"pagination": {
"total_count": 47,
"items_per_page": 2,
"page": 2,
"has_more": true,
"pages": 24
},
"pagination_type": "offset",
"filter_attributes": {
"status": ["draft", "published", "archived"],
"category__name": ["Python", "DevOps", "Architecture"]
},
"search_columns": ["title", "body", "category__name"],
"order_columns": ["title", "created_at"]
}
```
#### Cursor pagination
```python
@router.get("/cursor")
async def list_articles_cursor(
session: SessionDep,
params: Annotated[
dict,
Depends(
ArticleCrud.cursor_paginate_params(
default_page_size=20,
max_page_size=100,
default_order_field=Article.created_at,
)
),
],
) -> CursorPaginatedResponse[ArticleRead]:
return await ArticleCrud.cursor_paginate(
session=session,
**params,
schema=ArticleRead,
)
```
![Cursor endpoint](img/image-2.webp)
**Example request (first page):**
```
GET /articles/cursor?items_per_page=2&search=fastapi&status=published
```
**Example response:**
```json
{
"items": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"created_at": "2026-03-15T10:22:00Z",
"title": "Getting started with FastAPI",
"status": "published",
"published": true,
"category_id": "f1e2d3c4-b5a6-7890-abcd-ef0987654321"
},
{
"id": "b2c3d4e5-f6a7-8901-bcde-f01234567891",
"created_at": "2026-03-10T08:14:00Z",
"title": "FastAPI dependency injection explained",
"status": "published",
"published": true,
"category_id": "f1e2d3c4-b5a6-7890-abcd-ef0987654321"
}
],
"pagination": {
"next_cursor": "eyJjcmVhdGVkX2F0IjogIjIwMjYtMDMtMTBUMDg6MTQ6MDBaIn0=",
"prev_cursor": null,
"items_per_page": 2,
"has_more": true
},
"pagination_type": "cursor",
"filter_attributes": {
"status": ["draft", "published", "archived"],
"category__name": ["Python", "DevOps", "Architecture"]
},
"search_columns": ["title", "body", "category__name"],
"order_columns": ["title", "created_at"]
}
```
Pass `next_cursor` as the `cursor` parameter to fetch the following page:
```
GET /articles/cursor?cursor=eyJjcmVhdGVkX2F0IjogIjIwMjYtMDMtMTBUMDg6MTQ6MDBaIn0=&items_per_page=2
```
#### Combined endpoint
You can also expose a single endpoint that supports both strategies via a `pagination_type` query parameter:
```python
@router.get("/")
async def list_articles(
session: SessionDep,
params: Annotated[
dict,
Depends(
ArticleCrud.paginate_params(
default_page_size=20,
max_page_size=100,
default_order_field=Article.created_at,
)
),
],
) -> PaginatedResponse[ArticleRead]:
return await ArticleCrud.paginate(
session,
**params,
schema=ArticleRead,
)
```
![Combined endpoint](img/image-3.webp)
The response shape adapts to the chosen strategy. With `pagination_type=offset` (default):
```
GET /articles/?pagination_type=offset&page=1&items_per_page=2&status=published
```
```json
{
"items": [...],
"pagination": {
"total_count": 47,
"items_per_page": 2,
"page": 1,
"has_more": true
"pages": 24,
},
"pagination_type": "offset",
"filter_attributes": {
"status": ["draft", "published", "archived"],
"category__name": ["Python", "DevOps", "Architecture"]
},
"search_columns": ["title", "body", "category__name"],
"order_columns": ["title", "created_at"]
}
```
With `pagination_type=cursor`:
```
GET /articles/?pagination_type=cursor&items_per_page=2&status=published
```
```json
{
"items": [...],
"pagination": {
"next_cursor": "eyJjcmVhdGVkX2F0IjogIjIwMjYtMDMtMTBUMDg6MTQ6MDBaIn0=",
"prev_cursor": null,
"items_per_page": 2,
"has_more": true
},
"pagination_type": "cursor",
"filter_attributes": {
"status": ["draft", "published", "archived"],
"category__name": ["Python", "DevOps", "Architecture"]
},
"search_columns": ["title", "body", "category__name"],
"order_columns": ["title", "created_at"]
}
```
## Conclusion
`fastapi-toolsets` removes the boilerplate of writing pagination, filtering and sorting from scratch every time. The [CrudFactory](https://fastapi-toolsets.d3vyce.fr/module/crud/#creating-a-crud-class) declaration is the single source of truth for what your API exposes — the routes just call it.
- Documentation: https://fastapi-toolsets.d3vyce.fr
- Source code: https://github.com/d3vyce/fastapi-toolsets
- Example code: https://github.com/d3vyce/fastapi-toolsets/tree/main/docs_src/examples/pagination_search

View File

@@ -0,0 +1,82 @@
---
title: "Fastapi-Toolsets"
date: 2026-01-25
slug: "fastapi-toolsets"
showAuthor: false
showWordCount: false
showReadingTime: false
showRelatedContent: false
showPagination: false
tags: ["python", "fastapi", "package", "toolsets"]
---
![overview](featured.png)
{{< github repo="d3vyce/fastapi-toolsets" >}}
> Production-ready utilities for FastAPI applications
A modular collection of production-ready utilities for FastAPI. Install only what you need — from async CRUD and database helpers to CLI tooling, Prometheus metrics, and pytest fixtures. Each module is independently installable via optional extras, keeping your dependency footprint minimal.
[![CI](https://github.com/d3vyce/fastapi-toolsets/actions/workflows/ci.yml/badge.svg)](https://github.com/d3vyce/fastapi-toolsets/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/d3vyce/fastapi-toolsets/graph/badge.svg)](https://codecov.io/gh/d3vyce/fastapi-toolsets)
[![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
---
**Documentation**: [https://fastapi-toolsets.d3vyce.fr](https://fastapi-toolsets.d3vyce.fr)
**Source Code**: [https://github.com/d3vyce/fastapi-toolsets](https://github.com/d3vyce/fastapi-toolsets)
---
## Installation
The base package includes the core modules (CRUD, database, schemas, exceptions, fixtures, dependencies, logging):
```bash
uv add fastapi-toolsets
```
Install only the extras you need:
```bash
uv add "fastapi-toolsets[cli]" # CLI (typer)
uv add "fastapi-toolsets[metrics]" # Prometheus metrics (prometheus_client)
uv add "fastapi-toolsets[pytest]" # Pytest helpers (httpx, pytest-xdist)
```
Or install everything:
```bash
uv add "fastapi-toolsets[all]"
```
## Features
### Core
- **CRUD**: Generic async CRUD operations with `CrudFactory`, built-in full-text/faceted search and Offset/Cursor pagination.
- **Database**: Session management, transaction helpers, table locking, and polling-based row change detection
- **Dependencies**: FastAPI dependency factories (`PathDependency`, `BodyDependency`) for automatic DB lookups from path or body parameters
- **Fixtures**: Fixture system with dependency management, context support, and pytest integration
- **Standardized API Responses**: Consistent response format with `Response`, `PaginatedResponse`, and `PydanticBase`
- **Exception Handling**: Structured error responses with automatic OpenAPI documentation
- **Logging**: Logging configuration with uvicorn integration via `configure_logging` and `get_logger`
### Optional
- **CLI**: Django-like command-line interface with fixture management and custom commands support
- **Metrics**: Prometheus metrics endpoint with provider/collector registry
- **Pytest Helpers**: Async test client, database session management, `pytest-xdist` support, and table cleanup utilities
## License
MIT License - see [LICENSE](LICENSE) for details.
## Contributing
Contributions are welcome! Please feel free to submit issues and pull requests.

View File

@@ -1,12 +0,0 @@
FROM golang:1.23-alpine AS build
ARG HUGO_VERSION
ARG CGO=1
ENV CGO_ENABLED=${CGO}
ENV GOOS=linux
ENV GO111MODULE=on
RUN apk update && \
apk add --no-cache gcc musl-dev g++ git
RUN go install -tags extended github.com/gohugoio/hugo@${HUGO_VERSION}

View File

@@ -2,7 +2,7 @@
{{ $disableImageOptimization := .Site.Params.disableImageOptimization | default false }} {{ $disableImageOptimization := .Site.Params.disableImageOptimization | default false }}
<article class="flex flex-col items-center justify-center text-center"> <article class="flex flex-col items-center justify-center text-center">
<header class="flex flex-col items-center mb-3"> <header class="flex flex-col items-center mb-3">
{{ with .Site.Author.image }} {{ with .Site.Params.Author.image }}
{{ $authorImage := resources.Get . }} {{ $authorImage := resources.Get . }}
{{ if $authorImage }} {{ if $authorImage }}
{{ if not $disableImageOptimization }} {{ if not $disableImageOptimization }}
@@ -12,13 +12,13 @@
class="mb-2 rounded-full h-36 w-36" class="mb-2 rounded-full h-36 w-36"
width="144" width="144"
height="144" height="144"
alt="{{ $.Site.Author.name | default "Author" }}" alt="{{ $.Site.Params.Author.name | default "Author" }}"
src="{{ $authorImage.RelPermalink }}" src="{{ $authorImage.RelPermalink }}"
/> />
{{ end }} {{ end }}
{{ end }} {{ end }}
<h1 class="text-4xl font-extrabold"> <h1 class="text-4xl font-extrabold">
{{ .Site.Author.name | default .Site.Title }} {{ .Site.Params.Author.name | default .Site.Title }}
</h1> </h1>
<div class="mt-1 text-2xl"> <div class="mt-1 text-2xl">
{{ partialCached "author-links.html" . }} {{ partialCached "author-links.html" . }}

View File

@@ -1,7 +1,7 @@
{{ $disableImageOptimization := .Site.Params.disableImageOptimization | default false }} {{ $disableImageOptimization := .Site.Params.disableImageOptimization | default false }}
<article class="flex flex-col items-center justify-center text-center"> <article class="flex flex-col items-center justify-center text-center">
<header class="flex flex-col items-center mb-3"> <header class="flex flex-col items-center mb-3">
{{ with .Site.Author.image }} {{ with .Site.Params.Author.image }}
{{ $authorImage := resources.Get . }} {{ $authorImage := resources.Get . }}
{{ if $authorImage }} {{ if $authorImage }}
{{ if not $disableImageOptimization }} {{ if not $disableImageOptimization }}
@@ -11,15 +11,15 @@
class="mb-2 rounded-full h-36 w-36" class="mb-2 rounded-full h-36 w-36"
width="144" width="144"
height="144" height="144"
alt="{{ $.Site.Author.name | default "Author" }}" alt="{{ $.Site.Params.Author.name | default "Author" }}"
src="{{ $authorImage.RelPermalink }}" src="{{ $authorImage.RelPermalink }}"
/> />
{{ end }} {{ end }}
{{ end }} {{ end }}
<h1 class="text-4xl font-extrabold"> <h1 class="text-4xl font-extrabold">
{{ .Site.Author.name | default .Site.Title }} {{ .Site.Params.Author.name | default .Site.Title }}
</h1> </h1>
{{ with .Site.Author.headline }} {{ with .Site.Params.Author.headline }}
<h2 class="text-xl text-neutral-500 dark:text-neutral-400"> <h2 class="text-xl text-neutral-500 dark:text-neutral-400">
{{ . | markdownify | emojify }} {{ . | markdownify | emojify }}
</h2> </h2>