Auto-CRUD¶
Auto-CRUD is the fastest path to a production-ready REST API for a model. Two lines declare a complete CRUD surface — no boilerplate use cases needed.
Minimal example¶
from loom.core.model import ColumnField, TimestampedModel
from loom.rest.model import PaginationMode, RestInterface
class Product(TimestampedModel):
__tablename__ = "products"
id: int = ColumnField(primary_key=True, autoincrement=True)
sku: str = ColumnField(length=64, unique=True, index=True)
name: str = ColumnField(length=150)
price_cents: int = ColumnField()
stock: int = ColumnField()
class ProductInterface(RestInterface[Product]):
prefix = "/products"
tags = ("Products",)
auto = True # ← one flag, full CRUD
pagination_mode = PaginationMode.CURSOR
That declaration generates five routes automatically:
Method |
Path |
Description |
|---|---|---|
|
|
Paginated list (cursor or offset) |
|
|
Fetch by primary key |
|
|
Create new entity |
|
|
Partial update |
|
|
Delete by primary key |
No use-case classes, no route declarations, no wiring code.
Restrict which routes are generated¶
Use include to expose only a subset:
class ProductInterface(RestInterface[Product]):
prefix = "/products"
tags = ("Products",)
auto = True
include = ("get", "list", "update") # skips create and delete
pagination_mode = PaginationMode.CURSOR
Valid values: "get", "list", "create", "update", "delete".
Multiple read profiles¶
Expose a profile query parameter so callers choose the loading depth:
class ProductInterface(RestInterface[Product]):
prefix = "/products"
tags = ("Products",)
auto = True
profile_default = "default"
allowed_profiles = ("default", "with_details")
expose_profile = True
pagination_mode = PaginationMode.CURSOR
A ?profile=with_details query parameter is accepted on GET and list routes.
Mix auto-CRUD with custom routes¶
Use build_auto_routes() to inject auto-generated routes alongside explicit ones:
from loom.rest.autocrud import build_auto_routes
from loom.rest.model import PaginationMode, RestInterface, RestRoute
from app.product.model import Product
from app.product.use_cases import ListLowStockProductsUseCase, DispatchRestockEmailUseCase
class ProductInterface(RestInterface[Product]):
prefix = "/products"
tags = ("Products",)
pagination_mode = PaginationMode.CURSOR
routes = (
RestRoute(
use_case=ListLowStockProductsUseCase,
method="GET",
path="/low-stock",
summary="List low stock products",
),
RestRoute(
use_case=DispatchRestockEmailUseCase,
method="POST",
path="/{product_id}/jobs/restock-email",
summary="Dispatch restock email job",
status_code=202,
),
*build_auto_routes(Product, ()), # ← full CRUD appended
)
build_auto_routes(model, include) accepts an include tuple to restrict generated
routes — pass () for all five.
Bootstrap¶
Wire the interface to FastAPI via the YAML config:
# config/api.yaml
app:
name: my_store
code_path: src
discovery:
mode: modules
modules:
include:
- app.product.model
- app.product.interface
rest:
backend: fastapi
title: My Store API
version: 0.1.0
database:
url: ${oc.env:DATABASE_URL,sqlite+aiosqlite:///store.db}
# main.py
from loom.rest.fastapi.auto import create_app
app = create_app("config/api.yaml")
Three lines of application code. The framework handles DB wiring, DI, and route mounting.
When to add explicit use cases¶
Use auto-CRUD when the entity has no invariants beyond basic CRUD mechanics.
Add explicit UseCase classes when you need:
Field validation (
Rule) or derived fields (Compute)Cross-entity checks (
Exists,LoadById)Job dispatch or workflow orchestration
Custom authorization or multi-step sequences
See Use-case DSL for patterns that compose on top of auto-CRUD.
Full working example¶
The companion repository dummy-loom
shows auto-CRUD alongside custom use cases, background jobs, and Celery workers in a
runnable PostgreSQL application. Start it with:
git clone https://github.com/the-reacher-data/dummy-loom
cd dummy-loom
make up # starts postgres + redis + API + celery worker + flower