625 lines
No EOL
17 KiB
Markdown
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 |