kanban-app/docs/usage_rules_backend.md
2026-02-24 19:32:35 +03:00

17 KiB

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
# ✅ 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
# ✅ 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
# ✅ 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()
# ✅ 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
# ✅ 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
# ✅ 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
# ✅ 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
# ✅ 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
# ✅ 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

# ✅ 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

# ✅ 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

# ✅ 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

# ✅ 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

# ✅ 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:

# ✅ 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
# ✅ 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

# 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

# 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
# ✅ 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

# ✅ 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

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

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:

# 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