backend-fastapi
Skill0 starsChanged 6 months agoMITView on GitHub
Backend FastAPI skill cho multi-tenant SaaS với SQLAlchemy 2.0 async + PostgreSQL + Alembic. AUTO-TRIGGER khi: chạm file backend/app/**/*.py, file có `from fastapi`, `from sqlalchemy`, file Alembic migration, hoặc user nói "viết API/endpoint/model/migration/service" + "backend/route/schema/repository". Force apply 7 luật cứng: shop_id filter mọi query, Decimal money KHÔNG float, async/httpx KHÔNG requests, atomic stock update, order state machine validate transitions, JWT auth + permission check, env vars qua settings KHÔNG hardcode. Distilled từ Commerce Platform production patterns (60K+ lines, 100 commits). KHÔNG dùng cho: Express/Node backend, Django, frontend, mobile, infra/docker.
- Reads credentials
2 other projects use a version of this.
Adopted by
Most recently taken up by
- carlosedm10/skillstheir own version, first seen Aug 8, 2026
- Danifelipaez/NeoCafeIAtheir own version, first seen Apr 19, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- carlosedm10/skills★ 1same name
- Danifelipaez/NeoCafeIA★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository12 other files it gives its agents.
- brief-recapSkill
- code-reviewSkill
- debug-firstSkill
- deploySkill
- flutter-architectureSkill
- frontend-designSkill
- git-prSkill
- harnessSkill
- kimi-renderSkill
- lme-flutterSkill
- mobile-designSkill
- web-tasteSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 3 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 211
Default values: chỉ cho non-secret (port, log level)
- Very specific: line 234
Default lazy strategy: lazy="noload" ở model (PHÒNG NGỪA accidental N+1)
- Distinctive: line 331
NOT NULL column thêm vào table có data → 2-step (add nullable → backfill → set NOT NULL)
- Very specific: line 332
Add index trên large table → CREATE INDEX CONCURRENTLY (PostgreSQL)
- Distinctive: line 334
Test migration trên staging clone DB trước production
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Backend FastAPI Skill — Multi-Tenant SaaS Patterns
Triết lý: Backend không "đẹp" — backend đúng. Đúng nghĩa là: data isolation, money precision, race-condition-free, traceable error. Source: distilled từ Commerce Platform 60K+ lines production code.
1. AUTO-TRIGGER
DÙNG khi:
✓ Chạm file: backend/**/*.py, app/api/**, app/services/**, app/models/**, app/schemas/**
✓ File có import: from fastapi, from sqlalchemy, from pydantic, from alembic
✓ User nói "viết API/endpoint/route/model/migration/service/repository/schema"
✓ User nói "thêm field/relationship/query/transaction"
✓ Project có pyproject.toml hoặc requirements.txt với fastapi+sqlalchemy
KHÔNG dùng khi:
✗ Express/Node/Django backend
✗ Frontend (.tsx/.jsx) — dùng web-taste
✗ Mobile (.dart/.swift) — dùng mobile-design
✗ Infra (docker/CI) — không UI/business logic2. BẢY LUẬT CỨNG (KHÔNG ĐƯỢC PHÁ)
LUẬT 1 — Multi-tenant: MỌI query có shop_id filter
# ✗ SAI — lộ data shop khác
products = await db.execute(select(Product))
# ✓ ĐÚNG — luôn filter
products = await db.execute(
select(Product).where(Product.shop_id == current_shop.id)
)Rules:
- Mọi
select/update/deletePHẢI cówhere(Model.shop_id == current_shop.id) - Return 404 nếu resource không thuộc shop (KHÔNG 403 — tránh lộ existence)
- User có thể own nhiều Shop — Shop là tenant chính, KHÔNG phải User
- Composite query: filter shop_id ở MỌI table joined
# Composite query
result = await db.execute(
select(Order)
.join(Customer)
.where(
Order.shop_id == current_shop.id,
Customer.shop_id == current_shop.id, # PHẢI filter cả join table
Order.id == order_id,
)
)LUẬT 2 — Tiền: Decimal + Numeric(14,2), KHÔNG float
# ✗ SAI — float = sai số tích lũy
price = 250000.0 * 0.9 # 225000.00000000003
# ✓ ĐÚNG — Decimal precision
from decimal import Decimal
price = Decimal("250000") * Decimal("0.9") # 225000.0Rules:
- Database column:
Numeric(14, 2)cho mọi cột tiền - Python:
decimal.Decimalmọi tính toán - VNPay amount precision: dùng
.quantize(Decimal("1"))trướcint() - Order total = subtotal − discount + shipping (TẤT CẢ Decimal)
# VNPay amount fix (bug đã gặp)
amount_vnp = int(order.total.quantize(Decimal("1")) * 100)LUẬT 3 — Async everywhere, httpx KHÔNG requests
# ✗ SAI — block async event loop
import requests
result = requests.get(url)
# ✓ ĐÚNG — async
import httpx
async with httpx.AsyncClient() as client:
result = await client.get(url)Rules:
- Backend 100% async (asyncpg, httpx, aiofiles)
- KHÔNG import
requests - SMTP blocking →
await loop.run_in_executor(None, smtplib_send, msg) - Database session:
AsyncSessiontừ SQLAlchemy 2.0 - Endpoint:
async defluôn
LUẬT 4 — Inventory: atomic SQL, KHÔNG read-then-write
# ✗ SAI — race condition
product.stock -= quantity
await db.commit()
# ✓ ĐÚNG — atomic update với optimistic lock
result = await db.execute(
update(ProductVariant)
.where(
ProductVariant.id == variant_id,
ProductVariant.stock >= quantity, # CHECK trong query
)
.values(stock=ProductVariant.stock - quantity)
.returning(ProductVariant.stock)
)
if result.rowcount == 0:
raise HTTPException(400, "Không đủ hàng trong kho")Rules:
- Trừ kho bằng atomic SQL với
>=check →rowcount == 0= không đủ - Cộng kho khi cancel/return (atomic + log)
- Log mọi thay đổi vào
inventory_logs(who, when, why) - KHÔNG
product.stock -= nrồi commit
LUẬT 5 — Order state machine: KHÔNG nhảy trạng thái
VALID_TRANSITIONS = {
"pending": ["confirmed", "cancelled"],
"confirmed": ["packing", "cancelled"],
"packing": ["shipping", "cancelled"],
"shipping": ["delivered", "returned"],
"delivered": ["completed", "returned"],
"completed": [], # terminal
"returned": ["refunded"],
"refunded": [], # terminal
"cancelled": [], # terminal
}
def validate_transition(current: str, next_status: str) -> bool:
return next_status in VALID_TRANSITIONS.get(current, [])
# Sử dụng
if not validate_transition(order.status, new_status):
raise HTTPException(400, f"Không thể chuyển từ {order.status} sang {new_status}")Rules:
- Mọi thay đổi status PHẢI qua
validate_transition() - Log vào
order_status_logs(who, when, from, to, note) - KHÔNG bao giờ update status bằng raw SQL bypass
- Trigger notifications (email/SMS) sau khi log thành công
LUẬT 6 — Auth: JWT + shop ownership + role check
@router.get("/shops/{shop_id}/products")
async def list_products(
shop_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user), # JWT decode
shop: Shop = Depends(get_current_shop), # ownership verify
):
# shop.id == shop_id và user.id has access đã được verified
...Rules:
- Mọi admin endpoint: 3-layer guard (JWT + shop ownership + role)
- Return 404 thay vì 403 cho not-owned resource
- Public storefront API: NO auth nhưng GIỚI HẠN data (không trả
cost_price,internal_note) - Customer auth khác seller auth — endpoint khác
/customer/auth/*
LUẬT 7 — Env vars qua Settings, KHÔNG hardcode
# ✗ SAI
JWT_SECRET = "hardcoded-secret-123"
# ✓ ĐÚNG — pydantic Settings
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
JWT_SECRET: str
DATABASE_URL: str
GROQ_API_KEY: str = ""
model_config = {"env_file": ".env", "case_sensitive": True}
settings = Settings() # raise if missingRules:
- Mọi secret/config qua
settings .envtrong.gitignore- Production secrets: env vars trên Railway/Vercel UI, không commit
- Default values: chỉ cho non-secret (port, log level)
3. PATTERNS BẮT BUỘC
Lazy loading: prevent N+1
# ✗ SAI — N+1 queries
products = await db.execute(select(Product).where(...))
for p in products.scalars():
print(p.variants) # mỗi access = 1 query mới!
# ✓ ĐÚNG — eager load
products = await db.execute(
select(Product)
.options(selectinload(Product.variants)) # 1 extra query cho ALL variants
.where(...)
)Rules:
- Default lazy strategy:
lazy="noload"ở model (PHÒNG NGỪA accidental N+1) - Endpoint nào cần relationship → explicit
selectinload(...)hoặcjoinedload(...) - KHÔNG
lazy="select"(default) — dễ tạo N+1 silent - 1-many:
selectinload(2 queries, fast) - 1-1:
joinedload(1 query với JOIN)
Snapshot pattern (OrderItem giữ giá tại thời điểm)
class OrderItem(Base):
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"))
product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
variant_id: Mapped[int | None] = mapped_column(ForeignKey("product_variants.id"))
# Snapshot fields — KHÔNG join product để lấy
product_name: Mapped[str] = mapped_column(String(500))
variant_name: Mapped[str | None] = mapped_column(String(255))
price: Mapped[Decimal] = mapped_column(Numeric(14, 2)) # giá tại thời điểm đặt
quantity: Mapped[int] = mapped_column(Integer)
total: Mapped[Decimal] = mapped_column(Numeric(14, 2)) # price * quantityTại sao: Product giá thay đổi sau khi đặt → join product = sai lịch sử. Snapshot = source of truth.
Pagination chuẩn
@router.get("/products")
async def list_products(
page: int = 1,
page_size: int = 20,
search: str | None = None,
sort_by: str = "created_at",
sort_order: str = "desc",
...
):
query = select(Product).where(Product.shop_id == shop.id)
if search:
query = query.where(Product.name.ilike(f"%{search}%"))
total = await db.scalar(select(func.count()).select_from(query.subquery()))
sort_col = getattr(Product, sort_by)
if sort_order == "desc":
sort_col = sort_col.desc()
query = query.order_by(sort_col).offset((page - 1) * page_size).limit(page_size)
items = (await db.execute(query)).scalars().all()
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size,
"total_pages": (total + page_size - 1) // page_size,
}Error handling: clean JSON, no traceback leak
# Custom error handler
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger.error(f"Unhandled: {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={"detail": "Lỗi hệ thống. Vui lòng thử lại sau."},
)Rules:
- Production NEVER trả traceback ra response
- Log full traceback với
logger.error(..., exc_info=True) - User-facing message: tiếng Việt CÓ DẤU
- Status codes chính xác: 400 (validation), 401 (auth), 403 (permission), 404 (not found), 409 (conflict), 500 (server)
4. ALEMBIC MIGRATION SAFETY
# Generate migration
alembic revision --autogenerate -m "add new_field to products"
# REVIEW migration trước khi apply (tự động generate có thể sai!)
# Chú ý: data migrations PHẢI viết tay — autogenerate KHÔNG tạo data ops
# Apply
alembic upgrade head
# Rollback (nếu cần)
alembic downgrade -1Rules:
- LUÔN review autogen migration trước khi commit
- NOT NULL column thêm vào table có data → 2-step (add nullable → backfill → set NOT NULL)
- Add index trên large table →
CREATE INDEX CONCURRENTLY(PostgreSQL) - Drop column → 2-step (deploy code không dùng → drop column ở migration sau)
- Test migration trên staging clone DB trước production
5. AI INTEGRATION (9router pattern)
# ai_gateway.py — multi-provider fallback
class AIGateway:
async def chat_completion(self, messages, **kwargs):
# Try Groq first (free, fast)
try:
return await self._call_groq(messages, **kwargs)
except RateLimitError:
pass
# Fallback Gemini
try:
return await self._call_gemini(messages, **kwargs)
except RateLimitError:
pass
# Fallback DeepSeek
return await self._call_deepseek(messages, **kwargs)Rules:
- KHÔNG gọi trực tiếp 1 AI provider trong service code
- Luôn qua gateway (fallback + cache + log)
- Log usage vào
ai_usage_logs(shop_id, feature, tokens, cost, provider) - Rate limit theo subscription tier (free: 10/day, starter: 100, pro: 1000)
- Parse JSON response defensively (
_parse_json_from_response()với try/except)
6. CELERY BACKGROUND TASKS
# tasks/email_tasks.py
@celery_app.task(bind=True, max_retries=3)
def send_order_email_task(self, order_id: int):
try:
# email logic
...
except SMTPException as exc:
raise self.retry(exc=exc, countdown=60 * (self.request.retries + 1))Rules:
- Đừng làm những thứ này TRONG request — dùng Celery:
- Email sending (smtplib blocks)
- AI generation (1-30s latency)
- Excel export (memory heavy)
- Analytics aggregation
- Retry với exponential backoff
- Idempotent task design (chạy 2 lần không tạo 2 emails)
7. ANTI-PATTERNS — KHÔNG ĐƯỢC LÀM
| ✗ Sai | ✓ Đúng | Lý do |
float cho tiền | Decimal / Numeric(14,2) | Sai số tích lũy |
| Query không có shop_id | Luôn filter | Lộ data shop khác |
| Nhảy order status | VALID_TRANSITIONS | Data inconsistent |
stock -= n rồi commit | Atomic UPDATE với check | Race condition |
requests.get() | httpx.AsyncClient() | Block event loop |
| Gọi AI provider trực tiếp | Qua AIGateway | Không có fallback/cache |
lazy="select" mặc định | selectinload explicit | N+1 queries silent |
| Hardcode secret | settings | Security |
try/except: pass | Log + raise | Nuốt bug |
| Return 403 cho not-found | Return 404 | Lộ existence |
| Lưu giá trực tiếp OrderItem | Snapshot price tại thời điểm | Giá đổi = sai lịch sử |
| Email/SMS trong request | Celery task | Request timeout |
| Migration autogen không review | Review trước apply | Generated SQL có thể sai |
8. TỰ KIỂM TRA SAU KHI VIẾT
□ python -m py_compile <file.py> pass?
□ Mọi query có shop_id filter?
□ Tiền dùng Decimal, KHÔNG float?
□ async/await đúng (không có sync trong async)?
□ Order status transition validate?
□ Inventory atomic update?
□ Endpoint có Depends(get_current_user)?
□ Storefront public API không lộ cost_price/internal_note?
□ Error message tiếng Việt CÓ DẤU?
□ AI gọi qua gateway (có fallback)?
□ Background task qua Celery (không trong request)?
□ Migration đã review (không autogen blind)?Bất kỳ ✗ → fix trước khi commit.
9. REFERENCE FILES
| File | Khi nào đọc |
references/checklist.md | Trước khi commit — full audit checklist |
references/migration-recipes.md | Khi viết Alembic migration phức tạp (NOT NULL backfill, index concurrently, data migration) |
10. PROJECT-SPECIFIC NOTES
Commerce Platform (60K+ lines)
- Backend:
backend/app/ - Models: 23 (Shop, Product, Order, Customer, ...)
- Endpoints: 29 routers, 90+ endpoints
- Services: 35 (order_service, ai_service, payment_service, ...)
- Port: 8001 (tránh conflict với Dynamic Pricing port 8000)
- Production: Railway Singapore
Common imports
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, delete, func
from sqlalchemy.orm import selectinload, joinedload
from decimal import Decimal
from datetime import datetime, timezone
import httpx
from app.core.database import get_db
from app.core.security import get_current_user
from app.core.permissions import get_current_shop, require_role
from app.models import Shop, Product, Order, Customer
from app.schemas.product import ProductCreate, ProductResponseDoes it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

