# Architecture Rules - API Endpoint Rules # Rules for app/api/v1/**/*.py files api_endpoint_rules: - id: "API-001" name: "Endpoint must use Pydantic models for request/response" severity: "error" description: | All API endpoints must use Pydantic models (BaseModel) for request bodies and response models. Never use raw dicts or SQLAlchemy models directly. WHY THIS MATTERS: - Type safety: Pydantic validates response structure at runtime - Documentation: OpenAPI/Swagger auto-generates accurate docs - Contract stability: Schema changes are explicit and reviewable - IDE support: Consumers get autocomplete and type hints - Prevents bugs: Field name mismatches caught immediately COMMON VIOLATION: Service returns dict, frontend expects different field names. Example: Service returns {"total_imports": 5} but frontend expects {"total": 5}. With response_model, this mismatch is caught immediately. SCHEMA LOCATION: All response schemas must be defined in models/schema/*.py, never inline in endpoint files. This ensures schemas are reusable and discoverable. pattern: file_pattern: - "app/api/v1/**/*.py" - "app/modules/*/routes/api/**/*.py" anti_patterns: - "return dict" - "-> dict" - "return db_object" - "return {" example_good: | # In models/schema/stats.py class ImportStatsResponse(BaseModel): total: int pending: int completed: int failed: int # In app/api/v1/admin/marketplace.py @router.get("/stats", response_model=ImportStatsResponse) def get_import_statistics(db: Session = Depends(get_db)): return stats_service.get_import_statistics(db) example_bad: | # WRONG: No response_model, returns raw dict @router.get("/stats") def get_import_statistics(db: Session = Depends(get_db)): return stats_service.get_import_statistics(db) # Returns dict # WRONG: Schema defined inline in endpoint file class MyResponse(BaseModel): # Should be in models/schema/ ... @router.get("/data", response_model=MyResponse) def get_data(): ... - id: "API-002" name: "No Pydantic imports in API endpoint files" severity: "error" description: | API endpoint files must NOT import Pydantic directly (BaseModel, Field, etc.). All Pydantic schemas must be defined in models/schema/*.py and imported from there. WHY THIS MATTERS: - Separation of concerns: API handles HTTP, schemas handle data structure - Discoverability: All schemas in one place for easy review - Reusability: Schemas can be shared across endpoints - Prevents inline schema definitions that violate API-001 WRONG: from pydantic import BaseModel, Field class MyRequest(BaseModel): # Inline schema name: str RIGHT: # In models/schema/my_feature.py from pydantic import BaseModel class MyRequest(BaseModel): name: str # In app/api/v1/admin/my_feature.py from models.schema.my_feature import MyRequest pattern: file_pattern: - "app/api/v1/**/*.py" - "app/modules/*/routes/api/**/*.py" anti_patterns: - "from pydantic import" - "from pydantic.main import" example_good: | # In app/api/v1/admin/vendors.py from models.schema.vendor import ( LetzshopExportRequest, LetzshopExportResponse, ) @router.post("/export", response_model=LetzshopExportResponse) def export_products(request: LetzshopExportRequest): ... example_bad: | # In app/api/v1/admin/vendors.py from pydantic import BaseModel # WRONG: Don't import pydantic here class LetzshopExportRequest(BaseModel): # WRONG: Define in models/schema/ include_inactive: bool = False - id: "API-003" name: "Endpoint must NOT contain business logic" severity: "error" description: | API endpoints should only handle HTTP concerns (validation, auth, response formatting). All business logic must be delegated to service layer. Transaction control (db.commit) IS allowed at endpoint level - this is the recommended pattern for request-scoped transactions. One request = one transaction. What's NOT allowed: - db.add() - creating entities is business logic - db.query() - complex queries are business logic - db.delete() - deleting entities is business logic pattern: file_pattern: - "app/api/v1/**/*.py" - "app/modules/*/routes/api/**/*.py" anti_patterns: - "db.add(" - "db.delete(" - "db.query(" - id: "API-004" name: "Endpoint must NOT raise ANY exceptions directly" severity: "error" description: | API endpoints should NOT raise exceptions directly. Endpoints are a thin orchestration layer that: 1. Accepts request parameters (validated by Pydantic) 2. Calls dependencies for authentication/authorization (deps.py raises exceptions) 3. Calls services for business logic (services raise domain exceptions) 4. Returns response (formatted by Pydantic) Exception raising belongs in: - Dependencies (app/api/deps.py) - authentication/authorization validation - Services (app/services/) - business logic validation The global exception handler catches all OrionException subclasses and converts them to appropriate HTTP responses. WRONG (endpoint raises exception): @router.get("/orders") def get_orders(current_user: User = Depends(get_current_vendor_api)): if not hasattr(current_user, "token_vendor_id"): # Redundant check raise InvalidTokenException("...") # Don't raise here return order_service.get_orders(db, current_user.token_vendor_id) RIGHT (dependency guarantees, endpoint trusts): @router.get("/orders") def get_orders(current_user: User = Depends(get_current_vendor_api)): # Dependency guarantees token_vendor_id is present return order_service.get_orders(db, current_user.token_vendor_id) pattern: file_pattern: - "app/api/v1/**/*.py" - "app/modules/*/routes/api/**/*.py" anti_patterns: - "raise HTTPException" - "raise InvalidTokenException" - "raise InsufficientPermissionsException" - "if not hasattr\\(current_user.*token_vendor" exceptions: - "app/exceptions/handler.py" - id: "API-005" name: "Endpoint must have proper authentication/authorization" severity: "warning" description: | Protected endpoints must use Depends() for authentication. Use get_current_user, get_current_admin, etc. Auto-excluded files: - */auth.py - Authentication endpoints (login, logout, register) are intentionally public Public endpoint markers (place on line before or after decorator): - # public - Descriptive marker for intentionally unauthenticated endpoints - # noqa: API-005 - Standard noqa style to suppress warning Example: # public - Stripe webhook receives external callbacks @router.post("/webhook/stripe") def stripe_webhook(request: Request): ... pattern: file_pattern: - "app/api/v1/**/*.py" - "app/modules/*/routes/api/**/*.py" required_if_not_public: - "Depends(get_current_" auto_exclude_files: - "*/auth.py" public_markers: - "# public" - "# noqa: api-005" - id: "API-006" name: "Multi-tenant endpoints must scope queries to vendor_id" severity: "error" description: | All queries in vendor/storefront contexts must filter by vendor_id. Use request.state.vendor_id from middleware. pattern: file_pattern: - "app/api/v1/vendor/**/*.py" - "app/modules/*/routes/api/store*.py" - "app/api/v1/storefront/**/*.py" - "app/modules/*/routes/api/storefront*.py" discouraged_patterns: - "db.query(.*).all()" - id: "API-007" name: "API endpoints must NOT import database models directly" severity: "error" description: | API endpoints must follow the layered architecture: Routes → Services → Models. Routes should NEVER import database models directly. WHY THIS MATTERS: - Layered architecture: Each layer has clear responsibilities - Testability: Routes can be tested without database - Flexibility: Database schema changes don't affect route signatures - Type safety: Schemas define the API contract, not database structure For dependency injection (e.g., current user/customer), use schemas instead of database models as return types. WRONG: from models.database.customer import Customer from models.database.order import Order from app.modules.customers.models.customer import Customer @router.get("/orders") def get_orders(customer: Customer = Depends(get_current_customer)): ... RIGHT: from app.modules.customers.schemas import CustomerContext from app.modules.orders.services import order_service @router.get("/orders") def get_orders(customer: CustomerContext = Depends(get_current_customer)): return order_service.get_orders(db, customer.id, customer.vendor_id) ALLOWED IMPORTS IN API ROUTES: - Schemas: from models.schema.* or from app.modules.*.schemas - Services: from app.services.* or from app.modules.*.services - Dependencies: from app.api.deps - Database session: from app.core.database import get_db NOT ALLOWED: - from models.database.* - from app.modules.*.models.* pattern: file_pattern: - "app/api/**/*.py" - "app/modules/*/routes/api/**/*.py" anti_patterns: - "from models\\.database\\." - "from app\\.modules\\.[a-z_]+\\.models\\." exclude_files: - "app/api/deps.py" # Dependencies may need model access for queries