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()inbackend/app/__init__.py - NEVER initialize Flask extensions globally - create them at module level, initialize in
create_app() - NEVER create multiple SQLAlchemy instances - use the
dbextension fromapp/__init__.py
# ✅ CORRECT
from app import db
class User(db.Model):
pass
# ❌ WRONG
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
Import Patterns
- Import
dband other extensions fromappmodule, notflask_sqlalchemydirectly - Import models from the
app.modelspackage (e.g.,from app.models import User) - All routes should import
dbfromapp
# ✅ 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
dbfromapp: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_populatesfor 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()inbackend/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
dbthroughcurrent_app.extensions['sqlalchemy'].db - ALWAYS import
dbfromapp - Use
db.session.add()anddb.session.commit()for transactions - Use
db.session.flush()when you need the ID before commit - ALWAYS use
db.session.get(Model, id)instead ofModel.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.examplefor documentation
CORS
- Configure CORS in
create_app()using thecorsextension - Use
CORS_ORIGINSenvironment 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
dbfromapp
# ✅ 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(nottest_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.fixturefor 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 configurationclient: Test client for making HTTP requestsrunner: CLI runner for Flask commandsdb_session: Database session for database operationsadmin_user: Pre-created admin userregular_user: Pre-created regular userinactive_user: Pre-created inactive userproduct: Single productproducts: Multiple products (5 items)auth_headers: JWT headers for regular useradmin_headers: JWT headers for admin userorder: 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.securityfor password hashing - Use
set_password()andcheck_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
dbfromapp - All routes import
dbfromapp - 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