# Backend Development Rules for AI/LLM This document provides guidelines and rules that AI/LLM assistants should follow when writing or modifying backend code for the Crafting Shop application. ## Architecture Principles ### Application Factory Pattern - **ALWAYS** use the application factory pattern via `create_app()` in `backend/app/__init__.py` - **NEVER** initialize Flask extensions globally - create them at module level, initialize in `create_app()` - **NEVER** create multiple SQLAlchemy instances - use the `db` extension from `app/__init__.py` ```python # ✅ CORRECT from app import db class User(db.Model): pass # ❌ WRONG from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() ``` ### Import Patterns - Import `db` and other extensions from `app` module, not `flask_sqlalchemy` directly - Import models from the `app.models` package (e.g., `from app.models import User`) - All routes should import `db` from `app` ```python # ✅ CORRECT from app import db from app.models import User, Product, Order # ❌ WRONG from flask_sqlalchemy import SQLAlchemy from app.models.user import User # Don't import from individual files ``` ## Code Style ### Formatting - Use double quotes for strings and dictionary keys - Follow PEP 8 style guide - Use meaningful variable and function names - Maximum line length: 100 characters ### Type Hints - Add type hints to function signatures where appropriate - Use Python 3.11+ type hinting features ## Model Rules ### Database Models - **ALL** models must import `db` from `app`: `from app import db` - **ALL** models must inherit from `db.Model` - **NEVER** create your own `db = SQLAlchemy()` instance - Use `__tablename__` explicitly - Use `to_dict()` method for serialization - Use `__repr__()` for debugging ```python # ✅ CORRECT from app import db from datetime import datetime class Product(db.Model): __tablename__ = "products" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(200), nullable=False, index=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) def to_dict(self): return {"id": self.id, "name": self.name} ``` ### Relationships - Use `back_populates` for bidirectional relationships - Use `lazy="dynamic"` for one-to-many collections when appropriate - Define foreign keys explicitly with `db.ForeignKey()` ## Route/API Rules ### Blueprint Usage - **ALL** routes must be defined in blueprints - Register blueprints in `create_app()` in `backend/app/__init__.py` - Group related routes by functionality ### Request Handling - Use `request.get_json()` for JSON payloads - Always validate input data - Return proper HTTP status codes - Return JSON responses with `jsonify()` ```python # ✅ CORRECT from flask import Blueprint, request, jsonify from app import db api_bp = Blueprint("api", __name__) @api_bp.route("/products", methods=["POST"]) @jwt_required() def create_product(): data = request.get_json() if not data or not data.get("name"): return jsonify({"error": "Name is required"}), 400 product = Product(name=data["name"]) db.session.add(product) db.session.commit() return jsonify(product.to_dict()), 201 ``` ### Database Operations - **NEVER** access `db` through `current_app.extensions['sqlalchemy'].db` - **ALWAYS** import `db` from `app` - Use `db.session.add()` and `db.session.commit()` for transactions - Use `db.session.flush()` when you need the ID before commit - **ALWAYS** use `db.session.get(Model, id)` instead of `Model.query.get(id)` (SQLAlchemy 2.0) - Use `Model.query.get_or_404(id)` for 404 handling when appropriate ```python # ✅ CORRECT - SQLAlchemy 2.0 syntax from app import db from app.models import User user = db.session.get(User, user_id) # ❌ WRONG - Legacy syntax (deprecated) user = User.query.get(user_id) ``` ### Error Handling - Handle common errors (404, 400, 401, 403, 500) - Return JSON error responses with consistent format - Use Flask error handlers where appropriate ```python # ✅ CORRECT @app.errorhandler(404) def not_found(error): return jsonify({"error": "Not found"}), 404 ``` ### Authentication - Use `@jwt_required()` decorator for protected routes - Use `get_jwt_identity()` to get current user ID - Verify user permissions in routes ```python # ✅ CORRECT from flask_jwt_extended import jwt_required, get_jwt_identity @api_bp.route("/orders", methods=["POST"]) @jwt_required() def create_order(): user_id = get_jwt_identity() # ... rest of code ``` ## Configuration ### Environment Variables - **ALL** configuration must use environment variables via `os.environ.get()` - Provide sensible defaults in `app/config.py` - Use `.env.example` for documentation ### CORS - Configure CORS in `create_app()` using the `cors` extension - Use `CORS_ORIGINS` environment variable ```python # ✅ CORRECT cors.init_app(app, resources={r"/api/*": {"origins": app.config.get('CORS_ORIGINS', '*')}}) ``` ## Service Layer Rules ### Business Logic - Place complex business logic in `backend/app/services/` - Services should be stateless functions or classes - Services should import `db` from `app` ```python # ✅ CORRECT from app import db from app.models import Product def create_product_service(data): product = Product(**data) db.session.add(product) db.session.commit() return product ``` ## Testing Rules ### Test Structure - Use pytest framework - Place tests in `backend/tests/` - Use fixtures for common setup - Organize tests by functionality: `test_models.py`, `test_routes.py`, `test_schemas.py` ### Test Naming Conventions - Test files must start with `test_`: `test_products.py`, `test_users.py` - Test classes must start with `Test`: `TestProductModel`, `TestAuthRoutes` - Test functions must start with `test_`: `test_create_product`, `test_login_success` - Use descriptive names: `test_create_product_with_valid_data` (not `test_product`) ### Writing Tests #### Basic Test Structure ```python # ✅ CORRECT import pytest from app import db from app.models import Product class TestProductModel: """Test Product model""" @pytest.mark.unit def test_product_creation(self, db_session): """Test creating a product with valid data""" product = Product( name='Test Product', price=99.99 ) db_session.add(product) db_session.commit() assert product.id is not None assert product.name == 'Test Product' ``` #### Testing API Routes ```python # ✅ CORRECT def test_create_product(client, admin_headers): """Test creating a product as admin""" response = client.post('/api/products', headers=admin_headers, json={ 'name': 'New Product', 'price': 29.99 }) assert response.status_code == 201 data = response.get_json() assert data['name'] == 'New Product' assert 'password' not in data ``` #### Using Fixtures ```python # ✅ CORRECT - Use available fixtures def test_get_products(client, products): """Test getting all products""" response = client.get('/api/products') assert response.status_code == 200 data = response.get_json() assert len(data) == 5 # ❌ WRONG - Don't create fixtures manually in tests def test_get_products_wrong(client, db_session): products = [] for _ in range(5): p = Product(name='Test', price=10) db_session.add(p) products.append(p) db_session.commit() # ... use fixtures instead! ``` #### Testing Both Success and Failure Cases ```python # ✅ CORRECT - Test both scenarios def test_create_product_success(client, admin_headers): """Test creating product successfully""" response = client.post('/api/products', headers=admin_headers, json={'name': 'Test', 'price': 10}) assert response.status_code == 201 def test_create_product_unauthorized(client): """Test creating product without authentication""" response = client.post('/api/products', json={'name': 'Test', 'price': 10}) assert response.status_code == 401 def test_create_product_validation_error(client, admin_headers): """Test creating product with invalid data""" response = client.post('/api/products', headers=admin_headers, json={'name': 'Test', 'price': -10}) assert response.status_code == 400 ``` #### Parameterized Tests ```python # ✅ CORRECT - Use parameterization for similar tests @pytest.mark.parametrize("email,password,expected_status", [ ("user@example.com", "correct123", 200), ("wrong@email.com", "correct123", 401), ("user@example.com", "wrongpass", 401), ]) def test_login_validation(client, email, password, expected_status): """Test login with various invalid inputs""" response = client.post('/api/auth/login', json={ 'email': email, 'password': password }) assert response.status_code == expected_status ``` ### Test Markers Use appropriate markers for categorizing tests: ```python # ✅ CORRECT @pytest.mark.unit def test_user_creation(self, db_session): """Unit test - no HTTP, no external services""" pass @pytest.mark.integration def test_user_workflow(self, client): """Integration test - full request/response cycle""" pass @pytest.mark.auth def test_login(self, client): """Authentication-related test""" pass @pytest.mark.product def test_get_products(self, client): """Product-related test""" pass ``` ### Database in Tests - Use in-memory SQLite for tests - Clean up database between tests - Use `pytest.fixture` for database setup - **NEVER** use production database in tests - **NEVER** share state between tests ```python # ✅ CORRECT - Use db_session fixture def test_something(db_session): user = User(email='test@example.com') db_session.add(user) db_session.commit() # ❌ WRONG - Don't access db directly def test_something_wrong(): from app import db user = User(email='test@example.com') db.session.add(user) db.session.commit() ``` ### Available Fixtures Use these fixtures from `tests/conftest.py`: - **`app`**: Flask application instance with test configuration - **`client`**: Test client for making HTTP requests - **`runner`**: CLI runner for Flask commands - **`db_session`**: Database session for database operations - **`admin_user`**: Pre-created admin user - **`regular_user`**: Pre-created regular user - **`inactive_user`**: Pre-created inactive user - **`product`**: Single product - **`products`**: Multiple products (5 items) - **`auth_headers`**: JWT headers for regular user - **`admin_headers`**: JWT headers for admin user - **`order`**: Pre-created order with items ### Creating Custom Fixtures ```python # In tests/conftest.py or test file @pytest.fixture def custom_resource(db_session): """Create a custom test resource""" resource = CustomModel( name='Test Resource', value=100 ) db_session.add(resource) db_session.commit() return resource # Use in tests def test_custom_fixture(custom_resource): assert custom_resource.name == 'Test Resource' ``` ### Running Tests ```bash # Run all tests make test-backend # Run with coverage make test-backend-cov # Run with verbose output make test-backend-verbose # Run specific test file make test-backend-specific TEST=test_models.py # Run by marker make test-backend-marker MARKER=auth # Run only failed tests make test-backend-failed # Run in parallel (faster) make test-backend-parallel ``` ### Test Coverage Requirements - **Minimum 80%** code coverage required - **Critical paths** (auth, payments, data modification) must have >90% coverage - All new features must include tests ```python # ✅ CORRECT - Comprehensive test coverage def test_product_crud(self, client, admin_headers): """Test complete CRUD operations""" # Create response = client.post('/api/products', headers=admin_headers, json={'name': 'Test', 'price': 10}) assert response.status_code == 201 product_id = response.get_json()['id'] # Read response = client.get(f'/api/products/{product_id}') assert response.status_code == 200 # Update response = client.put(f'/api/products/{product_id}', headers=admin_headers, json={'name': 'Updated', 'price': 20}) assert response.status_code == 200 # Delete response = client.delete(f'/api/products/{product_id}', headers=admin_headers) assert response.status_code == 200 ``` ### Mocking External Services ```python # ✅ CORRECT - Mock external dependencies def test_external_api_call(client, mocker): """Test endpoint that calls external API""" mock_response = {'data': 'mocked data'} # Mock requests.get mock_get = mocker.patch('requests.get') mock_get.return_value.json.return_value = mock_response mock_get.return_value.status_code = 200 response = client.get('/api/external-data') assert response.status_code == 200 assert response.get_json() == mock_response mock_get.assert_called_once() ``` ### Test DOs and DON'Ts ✅ **DO:** - Use descriptive test names - Test both success and failure cases - Use fixtures for common setup - Mock external services - Keep tests independent - Use markers appropriately - Test edge cases and boundary conditions ❌ **DON'T:** - Share state between tests - Hardcode sensitive data (use faker) - Use production database - Skip error case testing - Write tests after deployment - Ignore slow tests in CI - Use complex setup in test methods (use fixtures instead) ## Security Rules ### Password Handling - **NEVER** store plain text passwords - Use `werkzeug.security` for password hashing - Use `set_password()` and `check_password()` methods ### SQL Injection Prevention - **ALWAYS** use SQLAlchemy ORM, never raw SQL - Use parameterized queries if raw SQL is absolutely necessary ### Input Validation - Validate all user inputs - Sanitize data before database operations - Use Flask-WTF for form validation ## File Structure for New Features When adding new features, follow this structure: ``` backend/app/ ├── models/ │ └── feature_name.py # Database models ├── routes/ │ └── feature_name.py # API routes ├── services/ │ └── feature_name.py # Business logic └── utils/ └── feature_name.py # Utility functions ``` ## Common Patterns ### Creating a New Model ```python from app import db from datetime import datetime class NewModel(db.Model): __tablename__ = "new_model" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(200), nullable=False, index=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) def to_dict(self): return { "id": self.id, "name": self.name, "created_at": self.created_at.isoformat() if self.created_at else None } ``` ### Creating New Routes ```python from flask import Blueprint, request, jsonify from flask_jwt_extended import jwt_required, get_jwt_identity from app import db from app.models import NewModel new_bp = Blueprint("new", __name__) @new_bp.route("/resources", methods=["GET"]) def get_resources(): resources = NewModel.query.all() return jsonify([r.to_dict() for r in resources]), 200 @new_bp.route("/resources", methods=["POST"]) @jwt_required() def create_resource(): data = request.get_json() resource = NewModel(**data) db.session.add(resource) db.session.commit() return jsonify(resource.to_dict()), 201 ``` ### Registering New Blueprints In `backend/app/__init__.py`: ```python # Import models from app.models.new_model import NewModel # Register blueprint from app.routes.feature_name import new_bp app.register_blueprint(new_bp, url_prefix="/api/new") ``` ## DO NOT DO List ❌ **NEVER** create `db = SQLAlchemy()` in model files ❌ **NEVER** access `db` via `current_app.extensions['sqlalchemy'].db` ❌ **NEVER** initialize Flask extensions before `create_app()` is called ❌ **NEVER** use raw SQL queries without proper escaping ❌ **NEVER** store secrets in code (use environment variables) ❌ **NEVER** return plain Python objects (use `jsonify()`) ❌ **NEVER** use single quotes for dictionary keys ❌ **NEVER** commit transactions without proper error handling ❌ **NEVER** ignore CORS configuration ❌ **NEVER** skip input validation ## Checklist Before Committing - [ ] All models import `db` from `app` - [ ] All routes import `db` from `app` - [ ] No `db = SQLAlchemy()` in model files - [ ] All routes are in blueprints - [ ] Proper error handling in place - [ ] Environment variables used for configuration - [ ] Type hints added to functions - [ ] Tests written for new functionality - [ ] Documentation updated - [ ] Code follows PEP 8 style guide