agentleFS
sonhaicodersonhaicoder/haiclaudeskill/skills/backend-fastapi/SKILL.md

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

  • Other versions2 projects use a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 2

    1. carlosedm10/skills★ 1same name
    2. 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.

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
  1. Distinctive:

    Default values: chỉ cho non-secret (port, log level)

    line 211
  2. Very specific:

    Default lazy strategy: lazy="noload" ở model (PHÒNG NGỪA accidental N+1)

    line 234
  3. Distinctive:

    NOT NULL column thêm vào table có data → 2-step (add nullable → backfill → set NOT NULL)

    line 331
  4. Very specific:

    Add index trên large table → CREATE INDEX CONCURRENTLY (PostgreSQL)

    line 332
  5. Distinctive:

    Test migration trên staging clone DB trước production

    line 334

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 logic

2. 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/delete PHẢ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.0

Rules:

  • Database column: Numeric(14, 2) cho mọi cột tiền
  • Python: decimal.Decimal mọi tính toán
  • VNPay amount precision: dùng .quantize(Decimal("1")) trước int()
  • 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: AsyncSession từ SQLAlchemy 2.0
  • Endpoint: async def luô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 -= n rồ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 missing

Rules:

  • Mọi secret/config qua settings
  • .env trong .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ặc joinedload(...)
  • 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 * quantity

Tạ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 -1

Rules:

  • 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✓ ĐúngLý do
float cho tiềnDecimal / Numeric(14,2)Sai số tích lũy
Query không có shop_idLuôn filterLộ data shop khác
Nhảy order statusVALID_TRANSITIONSData inconsistent
stock -= n rồi commitAtomic UPDATE với checkRace condition
requests.get()httpx.AsyncClient()Block event loop
Gọi AI provider trực tiếpQua AIGatewayKhông có fallback/cache
lazy="select" mặc địnhselectinload explicitN+1 queries silent
Hardcode secretsettingsSecurity
try/except: passLog + raiseNuốt bug
Return 403 cho not-foundReturn 404Lộ existence
Lưu giá trực tiếp OrderItemSnapshot price tại thời điểmGiá đổi = sai lịch sử
Email/SMS trong requestCelery taskRequest timeout
Migration autogen không reviewReview trước applyGenerated 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

FileKhi nào đọc
references/checklist.mdTrước khi commit — full audit checklist
references/migration-recipes.mdKhi 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, ProductResponse

Download now (ZIP)

Does 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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.