Code Standards
This document defines our coding standards based on the Four Rules: Predictability, Readability, Simplicity, and Upgradability.
The Four Rules
Every line of code we write should follow these principles:
1. Predictability
Every endpoint follows the same pattern. Every component has the same structure. When you’ve seen one, you’ve seen them all.
2. Readability
Code should tell a story. Anyone should be able to understand what’s happening without being an expert.
3. Simplicity
Minimal dependencies, clear separation of concerns. Solve today’s problems, not hypothetical future ones.
4. Upgradability
Version APIs, keep business logic in services, design for change.
Modern Tooling
We use Rust-based tools for maximum performance and developer productivity:
Python Tooling
- uv: Lightning-fast package manager (10-100x faster than pip)
- Ruff: All-in-one linter and formatter (replaces Black, isort, flake8)
- ty: Blazing-fast type checker by Astral (replaces mypy)
JavaScript/TypeScript Tooling
- Bun: Fast runtime and package manager (25x faster than npm)
- Vite + Rolldown: Rust-based bundler for instant builds
- SWC: Rust-based TypeScript compiler (20x faster than Babel)
- Biome: All-in-one linter and formatter (replaces ESLint + Prettier, 25x faster)
These tools enforce our standards automatically through pre-commit hooks and CI/CD.
Python/FastAPI Standards
Standard Endpoint Pattern
Always follow: Controller → Service → Model
# app/api/v1/endpoints/users.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.api import deps
from app.schemas.user import UserCreate, UserResponse
from app.services.user_service import UserService
from app.core.logging import get_logger
logger = get_logger(__name__)
router = APIRouter()
@router.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def create_user(
user_in: UserCreate,
db: Session = Depends(deps.get_db),
current_user: User = Depends(deps.get_current_user),
) -> UserResponse:
"""
Create a new user.
This endpoint creates a new user in the system with the provided details.
Args:
user_in: User creation data containing email, full_name, and password
db: Database session (injected)
current_user: Currently authenticated user (injected)
Returns:
UserResponse: The newly created user object
Raises:
HTTPException 403: If current user is not an admin
HTTPException 400: If email already exists
Example:
```python
POST /api/v1/users
{
"email": "john@example.com",
"full_name": "John Doe",
"password": "securepassword123"
}
```
"""
logger.info(f"Creating user with email: {user_in.email}")
# Authorisation check
if not current_user.is_admin:
logger.warning(f"Non-admin user {current_user.id} attempted to create user")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only administrators can create users"
)
# Call service layer
try:
user = UserService.create_user(db, user_in)
logger.info(f"User created successfully: {user.id}")
return user
except ValueError as e:
# Service layer raises ValueError for business logic errors
logger.error(f"Failed to create user: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
Service Layer Pattern
All business logic lives in services. Services are stateless and reusable.
# app/services/user_service.py
from typing import Optional
from sqlalchemy.orm import Session
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate
from app.core.security import get_password_hash
from app.core.logging import get_logger
logger = get_logger(__name__)
class UserService:
"""Service for managing user-related business logic."""
@staticmethod
def create_user(db: Session, user_in: UserCreate) -> User:
"""
Create a new user.
Args:
db: Database session
user_in: User creation schema
Returns:
User: The newly created user
Raises:
ValueError: If email already exists
"""
# Check if user already exists
existing_user = db.query(User).filter(User.email == user_in.email).first()
if existing_user:
logger.warning(f"Attempted to create duplicate user: {user_in.email}")
raise ValueError(f"User with email {user_in.email} already exists")
# Create user
user = User(
email=user_in.email,
full_name=user_in.full_name,
hashed_password=get_password_hash(user_in.password),
is_active=True,
is_admin=False,
)
db.add(user)
db.commit()
db.refresh(user)
logger.info(f"User created with ID: {user.id}")
return user
@staticmethod
def get_user_by_email(db: Session, email: str) -> Optional[User]:
"""
Retrieve a user by email.
Args:
db: Database session
email: User's email address
Returns:
Optional[User]: User if found, None otherwise
"""
return db.query(User).filter(User.email == email).first()
@staticmethod
def update_user(db: Session, user_id: int, user_in: UserUpdate) -> User:
"""
Update user details.
Args:
db: Database session
user_id: ID of user to update
user_in: Update data
Returns:
User: Updated user object
Raises:
ValueError: If user not found
"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise ValueError(f"User {user_id} not found")
# Update only provided fields
update_data = user_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(user, field, value)
db.commit()
db.refresh(user)
logger.info(f"User {user_id} updated")
return user
Dependency Injection Pattern
Use FastAPI’s dependency injection for database sessions, authentication, etc.
# app/api/deps.py
from typing import Generator
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from app.db.session import SessionLocal
from app.core.config import settings
from app.models.user import User
from app.core.logging import get_logger
logger = get_logger(__name__)
security = HTTPBearer()
def get_db() -> Generator:
"""
Get database session.
Yields:
Session: SQLAlchemy database session
"""
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_current_user(
db: Session = Depends(get_db),
credentials: HTTPAuthorizationCredentials = Depends(security),
) -> User:
"""
Get current authenticated user from JWT token.
Args:
db: Database session
credentials: HTTP Bearer token credentials
Returns:
User: Currently authenticated user
Raises:
HTTPException 401: If token is invalid or user not found
"""
token = credentials.credentials
try:
payload = jwt.decode(
token,
settings.JWT_SECRET_KEY,
algorithms=[settings.JWT_ALGORITHM]
)
user_id: int = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials"
)
except JWTError:
logger.warning("Invalid JWT token provided")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials"
)
user = db.query(User).filter(User.id == user_id).first()
if user is None:
logger.warning(f"User {user_id} from token not found in database")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found"
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Inactive user"
)
return user
Error Handling (4 Types)
Always use these specific HTTP status codes:
- 401 Unauthorized - Invalid or missing authentication
- 403 Forbidden - Valid authentication but insufficient permissions
- 422 Unprocessable Entity - Invalid input (handled automatically by Pydantic)
- 400 Bad Request - Business logic errors
# Example combining all error types
from fastapi import HTTPException, status
# 401 - Authentication failed
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials"
)
# 403 - Authorisation failed
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only admins can perform this action"
)
# 422 - Validation error (automatic with Pydantic)
# No code needed - FastAPI handles this
# 400 - Business logic error
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already exists"
)
Logging Pattern
Use structured JSON logging for all log messages.
# app/core/logging.py
import logging
import sys
from typing import Any
import json
class JSONFormatter(logging.Formatter):
"""Custom JSON formatter for structured logging."""
def format(self, record: logging.LogRecord) -> str:
log_data = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)
return json.dumps(log_data)
def get_logger(name: str) -> logging.Logger:
"""
Get a configured logger instance.
Args:
name: Logger name (usually __name__)
Returns:
logging.Logger: Configured logger
"""
logger = logging.getLogger(name)
if not logger.handlers:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
return logger
# Usage in code
logger = get_logger(__name__)
logger.info("User created", extra={"user_id": user.id, "email": user.email})
logger.error("Failed to process order", extra={"order_id": order_id, "error": str(e)})
Pydantic Schema Definitions
All request/response models use Pydantic for automatic validation.
# app/schemas/user.py
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, EmailStr, Field
class UserBase(BaseModel):
"""Base user schema with common fields."""
email: EmailStr = Field(..., description="User's email address")
full_name: str = Field(..., min_length=1, max_length=100, description="User's full name")
class UserCreate(UserBase):
"""Schema for creating a new user."""
password: str = Field(..., min_length=8, description="User's password (min 8 characters)")
class UserUpdate(BaseModel):
"""Schema for updating user details."""
full_name: Optional[str] = Field(None, min_length=1, max_length=100)
email: Optional[EmailStr] = None
class UserResponse(UserBase):
"""Schema for user response (excludes sensitive data)."""
id: int
is_active: bool
is_admin: bool
created_at: datetime
updated_at: Optional[datetime]
class Config:
from_attributes = True # Allows loading from ORM models
Docstring Requirements
Every function and class MUST have a Google-style docstring.
def process_shopify_order(order_id: str, db: Session) -> Order:
"""
Process a Shopify order and update inventory.
This function fetches order details from Shopify, validates the products,
updates inventory levels, and creates a fulfillment record.
Args:
order_id: The Shopify order ID
db: Database session for database operations
Returns:
Order: The processed order object
Raises:
ValueError: If order_id is invalid or order not found
HTTPException: If Shopify API call fails
Example:
```python
order = process_shopify_order("12345", db)
print(f"Order {order.id} processed successfully")
```
Note:
This function makes external API calls and may take 2-5 seconds.
It should be called from a background worker, not directly in API endpoints.
"""
# Implementation here
pass
React/TypeScript Standards
Functional Component Pattern
Always use functional components with hooks.
// src/components/UserProfile.tsx
import React, { useState, useEffect } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { userApi } from '../api/users';
/**
* User profile component displaying and editing user information.
*
* @component
* @example
* ```tsx
* <UserProfile userId={123} />
* ```
*/
interface UserProfileProps {
/** The ID of the user to display */
userId: number;
/** Callback when user is updated */
onUpdate?: (user: User) => void;
}
interface User {
id: number;
email: string;
full_name: string;
is_active: boolean;
created_at: string;
}
export const UserProfile: React.FC<UserProfileProps> = ({ userId, onUpdate }) => {
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState({ full_name: '' });
// Fetch user data
const {
data: user,
isLoading,
error,
refetch
} = useQuery({
queryKey: ['user', userId],
queryFn: () => userApi.getUser(userId),
});
// Update user mutation
const updateMutation = useMutation({
mutationFn: (data: { full_name: string }) => userApi.updateUser(userId, data),
onSuccess: (updatedUser) => {
setIsEditing(false);
refetch();
onUpdate?.(updatedUser);
},
});
// Initialize form when user data loads
useEffect(() => {
if (user) {
setFormData({ full_name: user.full_name });
}
}, [user]);
// Event handlers
const handleEdit = () => {
setIsEditing(true);
};
const handleCancel = () => {
setIsEditing(false);
if (user) {
setFormData({ full_name: user.full_name });
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
updateMutation.mutate(formData);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
// Loading state
if (isLoading) {
return (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
// Error state
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded p-4">
<p className="text-red-800">Failed to load user: {error.message}</p>
</div>
);
}
// No data state
if (!user) {
return <div>User not found</div>;
}
return (
<div className="bg-white shadow rounded-lg p-6">
<h2 className="text-2xl font-bold mb-4">User Profile</h2>
{!isEditing ? (
// View mode
<div className="space-y-3">
<div>
<label className="text-sm font-medium text-gray-600">Name</label>
<p className="text-gray-900">{user.full_name}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-600">Email</label>
<p className="text-gray-900">{user.email}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-600">Status</label>
<p className="text-gray-900">{user.is_active ? 'Active' : 'Inactive'}</p>
</div>
<button
onClick={handleEdit}
className="mt-4 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Edit Profile
</button>
</div>
) : (
// Edit mode
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="full_name" className="block text-sm font-medium text-gray-700">
Full Name
</label>
<input
type="text"
id="full_name"
name="full_name"
value={formData.full_name}
onChange={handleInputChange}
className="mt-1 block w-full rounded border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
required
/>
</div>
<div className="flex gap-2">
<button
type="submit"
disabled={updateMutation.isPending}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{updateMutation.isPending ? 'Saving...' : 'Save Changes'}
</button>
<button
type="button"
onClick={handleCancel}
className="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400"
>
Cancel
</button>
</div>
{updateMutation.isError && (
<div className="bg-red-50 border border-red-200 rounded p-3">
<p className="text-red-800 text-sm">
Failed to update: {updateMutation.error.message}
</p>
</div>
)}
</form>
)}
</div>
);
};
Props Interface Definition
Always define props interfaces with JSDoc comments.
/**
* Props for the OrderList component.
*/
interface OrderListProps {
/** Array of orders to display */
orders: Order[];
/** Callback when an order is selected */
onOrderSelect: (orderId: number) => void;
/** Optional CSS class name */
className?: string;
/** Whether to show the action buttons */
showActions?: boolean;
}
State Management
Use useState for component state, React Query for server state.
// Component state
const [searchQuery, setSearchQuery] = useState('');
const [selectedIds, setSelectedIds] = useState<number[]>([]);
// Server state with React Query
const { data, isLoading, error } = useQuery({
queryKey: ['orders', { status: 'pending', search: searchQuery }],
queryFn: () => orderApi.fetchOrders({ status: 'pending', search: searchQuery }),
});
Naming Conventions
Files
- Python:
snake_case.py(e.g.,user_service.py,shopify_webhook.py) - TypeScript/React:
PascalCase.tsxfor components (e.g.,UserProfile.tsx) - TypeScript/other:
camelCase.tsfor utilities (e.g.,apiClient.ts)
Functions
- Python:
snake_case(e.g.,create_user,process_order) - TypeScript:
camelCase(e.g.,createUser,processOrder) - React event handlers:
handle*(e.g.,handleClick,handleSubmit)
Variables
- Python:
snake_case(e.g.,user_id,order_total) - TypeScript:
camelCase(e.g.,userId,orderTotal)
Constants
- Python:
UPPER_SNAKE_CASE(e.g.,MAX_RETRY_ATTEMPTS,DEFAULT_TIMEOUT) - TypeScript:
UPPER_SNAKE_CASEorcamelCasefor config (e.g.,API_BASE_URL)
Classes
- Python:
PascalCase(e.g.,UserService,OrderModel) - TypeScript:
PascalCase(e.g.,ApiClient,UserProfile)
Import Ordering
Python
Ruff automatically sorts imports (replaces isort):
# Standard library imports
import json
import logging
from datetime import datetime
from typing import Optional, List
# Third-party imports
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
# Local application imports
from app.core.config import settings
from app.models.user import User
from app.schemas.user import UserCreate, UserResponse
from app.services.user_service import UserService
TypeScript
Biome automatically organizes imports:
// React imports
import React, { useState, useEffect } from 'react';
// Third-party library imports
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
// Local component imports
import { Button } from '../components/Button';
import { Modal } from '../components/Modal';
// Local utility/API imports
import { userApi } from '../api/users';
import { formatDate } from '../utils/date';
// Type imports
import type { User, Order } from '../types';
Documentation Requirements
When to Use Docstrings
Python: Every public function, class, and module
TypeScript: Every component, exported function, and complex logic
When to Use Comments
Use comments to explain why, not what:
# GOOD - Explains why
# We cache this for 1 hour because the Shopify API rate limit is 2 calls/second
cache.set(key, value, timeout=3600)
# BAD - Explains what (code already shows this)
# Set cache value
cache.set(key, value, timeout=3600)
API Contract Documentation
All public API endpoints MUST be documented in docs/api/endpoints.md.
Workflow:
- Design endpoint and write documentation first
- Implement endpoint following the documented contract
- Update documentation if implementation requires changes
See ../03-workflows/feature-development.md and ../api/endpoints.md.
Next Steps
- Review
api-patterns.mdfor API-specific patterns - Read
database-patterns.mdfor database design - Check
testing-standards.mdfor testing guidelines - Use templates in
../04-templates/as starting points