# api-patterns > FastAPI patterns and conventions for the backend. Use when working on API endpoints, SQLAlchemy models, Pydantic schemas, or backend tests. - Author: aarongbenjamin - Repository: mssindustries/product-customizer - Version: 20260201190204 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-06 - Source: https://github.com/mssindustries/product-customizer - Web: https://mule.run/skillshub/@@mssindustries/product-customizer~api-patterns:20260201190204 --- --- name: api-patterns description: FastAPI patterns and conventions for the backend. Use when working on API endpoints, SQLAlchemy models, Pydantic schemas, or backend tests. user-invocable: false --- # Backend API Patterns Reference for FastAPI patterns used in `src/backend/`. ## File Locations | Type | Location | |------|----------| | Routes | `src/backend/app/api/v1/routes/` | | Schemas | `src/backend/app/schemas/` | | Models | `src/backend/app/models/` | | Repositories | `src/backend/app/repositories/` | | Tests | `src/backend/tests/` | | Dependencies | `src/backend/app/api/deps.py` | | Exceptions | `src/backend/app/core/exceptions.py` | ## Repository Pattern Use repositories to eliminate duplicated data access logic. ```python # GOOD: Use repository with ensure_exists() product = await ProductRepository(db).ensure_exists(product_id) # BAD: Manual SELECT + None check result = await db.execute(select(Product).where(Product.id == product_id)) product = result.scalar_one_or_none() if product is None: raise HTTPException(status_code=404, detail="Product not found") ``` Key methods: - `ensure_exists(id)` - Returns entity or raises `EntityNotFoundError` - `list_paginated(skip, limit)` - Returns paginated results The global exception handler converts `EntityNotFoundError` to HTTP 404. ## Response Schema Factory Methods All response schemas have `from_model()` and `from_models()` class methods. ```python # GOOD: Use factory method return ProductResponse.from_model(product) return ProductResponse.from_models(products) # For collections # BAD: Manual field mapping return ProductResponse( id=product.id, name=product.name, # ... easy to miss fields or make typos ) ``` Benefits: - Single source of truth for ORM-to-schema conversion - Eliminates typos and missed fields - Easier maintenance when schemas change ## Dependency Injection Use the `DbSession` type alias from `app/api/deps.py`: ```python from app.api.deps import DbSession @router.get("/products/{product_id}") async def get_product(product_id: UUID, db: DbSession): product = await ProductRepository(db).ensure_exists(product_id) return ProductResponse.from_model(product) ``` ## Exception Handling Use domain exceptions from `app/core/exceptions.py`, not HTTPException: ```python # GOOD: Domain exception (caught by global handler) raise EntityNotFoundError("Product", product_id) # BAD: HTTP exception in business logic raise HTTPException(status_code=404, detail="Product not found") ``` ## Testing Patterns Available fixtures in `tests/conftest.py`: - `client` - AsyncClient for API endpoint tests - `db_session` - Database session for direct DB tests ```python @pytest.mark.asyncio async def test_get_product(client: AsyncClient, db_session: AsyncSession): # Create test data product = Product(name="Test") db_session.add(product) await db_session.commit() # Test endpoint response = await client.get(f"/api/v1/products/{product.id}") assert response.status_code == 200 assert response.json()["name"] == "Test" ``` Rules: - Each test gets a fresh database (SQLite in-memory) - Tests run in parallel - don't share state - Test files: `test_*.py`, functions: `test_*` ## Commands ```bash cd src/backend make test # Run tests with coverage make lint # Run linter make format # Auto-format code make check # Run lint + test ```