from enum import Enum
from typing import Optional
from fastapi import Query
from pydantic import BaseModel, Field, field_validator
class SortField(str, Enum):
name = "name"
price = "price"
created_at = "created_at"
class SortDir(str, Enum):
asc = "asc"
desc = "desc"
class ProductFilters(BaseModel):
search: Optional[str] = Field(Query(None, description="Free-text name search"))
category: Optional[str] = Field(Query(None))
in_stock: Optional[bool] = Field(Query(None))
sort: SortField = Field(Query(SortField.created_at))
order: SortDir = Field(Query(SortDir.desc))
page: int = Field(Query(1, ge=1))
page_size: int = Field(Query(20, ge=1, le=100))
@field_validator("search", "category")
@classmethod
def normalize_text(cls, value):
if value is None:
return None
cleaned = " ".join(value.split())
return cleaned or None
@property
def offset(self) -> int:
return (self.page - 1) * self.page_size
@classmethod
def as_dependency(
cls,
search: Optional[str] = Query(None),
category: Optional[str] = Query(None),
in_stock: Optional[bool] = Query(None),
sort: SortField = Query(SortField.created_at),
order: SortDir = Query(SortDir.desc),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
) -> "ProductFilters":
return cls(
search=search,
category=category,
in_stock=in_stock,
sort=sort,
order=order,
page=page,
page_size=page_size,
)
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from .filters import ProductFilters, SortDir
from .models import Product
class ProductRepository:
def __init__(self, session: AsyncSession):
self.session = session
def _apply_filters(self, stmt, filters: ProductFilters):
if filters.search:
stmt = stmt.where(Product.name.ilike(f"%{filters.search}%"))
if filters.category:
stmt = stmt.where(Product.category == filters.category)
if filters.in_stock is not None:
stmt = stmt.where(Product.in_stock.is_(filters.in_stock))
return stmt
async def list(self, filters: ProductFilters):
column = getattr(Product, filters.sort.value)
ordering = column.desc() if filters.order is SortDir.desc else column.asc()
stmt = self._apply_filters(select(Product), filters)
stmt = stmt.order_by(ordering).limit(filters.page_size).offset(filters.offset)
result = await self.session.execute(stmt)
return result.scalars().all()
async def count(self, filters: ProductFilters) -> int:
from sqlalchemy import func
stmt = self._apply_filters(select(func.count()).select_from(Product), filters)
result = await self.session.execute(stmt)
return int(result.scalar_one())
from typing import List
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from .db import get_session
from .filters import ProductFilters
from .repository import ProductRepository
from .schemas import ProductOut
router = APIRouter(prefix="/products", tags=["products"])
class PagedResponse(BaseModel):
items: List[ProductOut]
total: int
page: int
page_size: int
@router.get("", response_model=PagedResponse)
async def list_products(
filters: ProductFilters = Depends(ProductFilters.as_dependency),
session: AsyncSession = Depends(get_session),
):
repo = ProductRepository(session)
items = await repo.list(filters)
total = await repo.count(filters)
return PagedResponse(
items=items,
total=total,
page=filters.page,
page_size=filters.page_size,
)
Endpoints that list resources tend to accumulate a mess of loose query parameters: pagination, sorting, status filters, free-text search. Threading each one through the function signature and re-validating it by hand is repetitive and error-prone. This snippet centralizes all of that into a single Pydantic model that FastAPI injects as a dependency, so the endpoint receives one validated, normalized object instead of a dozen raw strings.
In filters.py, ProductFilters is a plain BaseModel whose fields map to query parameters. Each field uses Field(Query(...)) so FastAPI still documents them individually in OpenAPI and parses them from the query string, while Pydantic enforces bounds — page must be >= 1, page_size is clamped between 1 and 100. The SortField and SortDir enums restrict sort and order to known values, which turns a whole class of typos into automatic 422 responses. A field_validator on search trims whitespace and collapses empty strings to None, so downstream code never has to distinguish '' from a missing filter. The computed offset property derives the SQL offset from page and page_size, keeping that arithmetic in one place.
The key move is the as_dependency classmethod. FastAPI can use a class directly as a dependency, but wrapping it lets the model's own constructor run against the parsed query values, giving a clean typed instance rather than relying on implicit binding. Because it is a normal callable returning ProductFilters, it composes with Depends like any other provider.
In repository.py, ProductRepository.list accepts that validated object and does nothing but build a query — apply_search, ordering via getattr on the model column, and limit/offset from the already-safe values. No defensive checks are needed here because invalid input never reaches this layer.
In router.py, the endpoint signature is just filters: ProductFilters = Depends(ProductFilters.as_dependency). The handler stays tiny: it delegates to the repository and returns a PagedResponse echoing the normalized page and page_size. The trade-off is a small amount of indirection in exchange for one authoritative definition of what a valid filter set looks like, reusable across every list endpoint and fully reflected in the generated docs.
Related snips
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
from sklearn.linear_model import LogisticRegression
standard_pipeline = Pipeline([
('scaler', StandardScaler()),
Scaling and normalization choices for different model families
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
package com.example.myapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
Dependency injection with Hilt
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
Share this code
Here's the card — post it anywhere.