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

625 lines
No EOL
17 KiB
Markdown

# 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