Project Structure
Recommended Structure
Section titled “Recommended Structure”Zenith encourages a clean, scalable project structure that separates concerns and makes your codebase maintainable as it grows.
Directorymy-zenith-app/
Directoryapp/
- __init__.py
- main.py Entry point
- config.py Configuration management
Directoryservices/ Business logic
- __init__.py
- users.py
- products.py
- orders.py
Directorymodels/ Data models
- __init__.py
- user.py
- product.py
- order.py
Directoryroutes/ API endpoints
- __init__.py
- auth.py
- users.py
- products.py
- orders.py
Directorymiddleware/ Custom middleware
- __init__.py
- auth.py
- logging.py
Directoryexternal/ External services
- __init__.py
- email.py
- storage.py
- payment.py
Directoryutils/ Utility functions
- __init__.py
- validators.py
- formatters.py
Directorytests/
- conftest.py
- test_users.py
- test_products.py
- test_orders.py
Directorymigrations/ Database migrations
- alembic.ini
Directoryversions/
- …
Directorystatic/ Static files
Directorycss/
- …
Directoryjs/
- …
Directoryimages/
- …
Directorytemplates/ Email/HTML templates
Directoryemail/
- welcome.html
- order_confirmation.html
Directoryscripts/ Utility scripts
- seed_db.py
- cleanup.py
- .env Environment variables
- .env.example
- .gitignore
- docker-compose.yml
- Dockerfile
- pyproject.toml Project dependencies
- README.md
Core Components
Section titled “Core Components”Entry Point (main.py)
Section titled “Entry Point (main.py)”The main application file where you create and configure your Zenith app:
from zenith import Zenithfrom app.config import settingsfrom app.routes import auth, users, products, ordersfrom app.middleware import setup_middleware
# Create applicationapp = Zenith( title=settings.APP_NAME, version=settings.APP_VERSION, debug=settings.DEBUG)
# Setup middlewaresetup_middleware(app)
# Include routersapp.include_router(auth.router, prefix="/auth", tags=["Authentication"])app.include_router(users.router, prefix="/users", tags=["Users"])app.include_router(products.router, prefix="/products", tags=["Products"])app.include_router(orders.router, prefix="/orders", tags=["Orders"])
# Startup event@app.on_event("startup")async def startup(): """Initialize services on startup.""" from app.db import init_db await init_db()
# Health check@app.get("/health")async def health_check(): return {"status": "healthy", "version": settings.APP_VERSION}
if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)Configuration (config.py)
Section titled “Configuration (config.py)”Centralized configuration using environment variables:
from pydantic import BaseModel, Fieldfrom pydantic_settings import BaseSettingsfrom typing import Optional, List
class Settings(BaseSettings): """Application settings."""
# App settings APP_NAME: str = "My Zenith App" APP_VERSION: str = "1.0.0" DEBUG: bool = False ENVIRONMENT: str = Field(default="production", env="ENVIRONMENT")
# Database DATABASE_URL: str = Field(..., env="DATABASE_URL") DATABASE_POOL_SIZE: int = 20 DATABASE_MAX_OVERFLOW: int = 40
# Redis REDIS_URL: Optional[str] = Field(None, env="REDIS_URL")
# Security SECRET_KEY: str = Field(..., env="SECRET_KEY") ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 REFRESH_TOKEN_EXPIRE_DAYS: int = 7 BCRYPT_ROUNDS: int = 12
# CORS CORS_ORIGINS: List[str] = Field( default=["http://localhost:3000"], env="CORS_ORIGINS" )
# Email SMTP_HOST: Optional[str] = Field(None, env="SMTP_HOST") SMTP_PORT: int = 587 SMTP_USER: Optional[str] = Field(None, env="SMTP_USER") SMTP_PASSWORD: Optional[str] = Field(None, env="SMTP_PASSWORD")
# AWS AWS_ACCESS_KEY_ID: Optional[str] = Field(None, env="AWS_ACCESS_KEY_ID") AWS_SECRET_ACCESS_KEY: Optional[str] = Field(None, env="AWS_SECRET_ACCESS_KEY") AWS_REGION: str = "us-east-1" S3_BUCKET: Optional[str] = Field(None, env="S3_BUCKET")
class Config: env_file = ".env" case_sensitive = True
# Create settings instancesettings = Settings()Services (services/)
Section titled “Services (services/)”Business logic organized by domain:
from zenith import Service, Injectfrom app.models.user import User, UserCreate, UserUpdatefrom app.external.email import EmailServicefrom typing import Optional, List
class UserService(Service): """User business logic."""
def __init__(self, email_service: EmailService = Inject()): super().__init__() self.email_service = email_service
async def create_user(self, user_data: UserCreate) -> User: """Create a new user.""" # Check if email exists existing = await self.get_user_by_email(user_data.email) if existing: raise ValueError("Email already registered")
# Create user user = User(**user_data.model_dump()) user.set_password(user_data.password)
self.db.add(user) await self.db.commit() await self.db.refresh(user)
# Send welcome email await self.email_service.send_welcome_email(user)
return user
async def get_user_by_email(self, email: str) -> Optional[User]: """Get user by email.""" statement = select(User).where(User.email == email) result = await self.db.exec(statement) return result.first()Models (models/)
Section titled “Models (models/)”Data models using SQLModel:
from zenith.db import SQLModel, Fieldfrom typing import Optionalfrom datetime import datetime
class UserBase(SQLModel): """Base user model.""" email: str = Field(unique=True, index=True) username: str = Field(unique=True, index=True) full_name: Optional[str] = None is_active: bool = Field(default=True) is_superuser: bool = Field(default=False)
class User(UserBase, table=True): """User database model.""" id: Optional[int] = Field(default=None, primary_key=True) password_hash: str created_at: datetime = Field(default_factory=datetime.utcnow) updated_at: Optional[datetime] = None
# Relationships posts: List["Post"] = Relationship(back_populates="author") orders: List["Order"] = Relationship(back_populates="customer")
class UserCreate(UserBase): """User creation model.""" password: str = Field(min_length=8)
class UserUpdate(SQLModel): """User update model.""" email: Optional[str] = None username: Optional[str] = None full_name: Optional[str] = None password: Optional[str] = None
class UserResponse(UserBase): """User response model.""" id: int created_at: datetimeRoutes (routes/)
Section titled “Routes (routes/)”API endpoints organized by resource:
from zenith import Router, Injectfrom app.services.users import UserServicefrom app.models.user import User, UserCreate, UserUpdate, UserResponsefrom app.auth import get_current_userfrom typing import List
router = Router()
@router.get("/", response_model=List[UserResponse])async def list_users( skip: int = 0, limit: int = 100, users: UserService = Inject()): """List all users.""" return await users.list_users(skip=skip, limit=limit)
@router.post("/", response_model=UserResponse, status_code=201)async def create_user( user_data: UserCreate, users: UserService = Inject()): """Create a new user.""" return await users.create_user(user_data)
@router.get("/me", response_model=UserResponse)async def get_current_user( current_user: User = Inject(get_current_user)): """Get current user.""" return current_user
@router.put("/me", response_model=UserResponse)async def update_current_user( user_update: UserUpdate, current_user: User = Inject(get_current_user), users: UserService = Inject()): """Update current user.""" return await users.update_user(current_user.id, user_update)Environment Files
Section titled “Environment Files”Development (.env)
Section titled “Development (.env)”# ApplicationAPP_NAME="Zenith Dev"DEBUG=trueENVIRONMENT=development
# DatabaseDATABASE_URL=postgresql://zenith:zenith@localhost/zenith_dev
# RedisREDIS_URL=redis://localhost:6379
# SecuritySECRET_KEY=your-secret-key-for-development
# Email (using MailHog for dev)SMTP_HOST=localhostSMTP_PORT=1025Production (.env.production)
Section titled “Production (.env.production)”# ApplicationAPP_NAME="Zenith API"DEBUG=falseENVIRONMENT=production
# DatabaseDATABASE_URL=postgresql://user:pass@db.example.com/zenith_prod
# RedisREDIS_URL=redis://redis.example.com:6379
# SecuritySECRET_KEY=${SECRET_KEY} # From environment
# EmailSMTP_HOST=smtp.sendgrid.netSMTP_PORT=587SMTP_USER=apikeySMTP_PASSWORD=${SENDGRID_API_KEY}
# AWSAWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}S3_BUCKET=zenith-uploadsTesting Structure
Section titled “Testing Structure”Organize tests to mirror your application structure:
Directorytests/
Directoryunit/
Directoryservices/
- test_user_service.py
- test_product_service.py
Directorymodels/
- test_user_model.py
Directoryutils/
- test_validators.py
Directoryintegration/
- test_auth_flow.py
- test_order_process.py
Directorye2e/
- test_api_endpoints.py
Directoryfixtures/
- users.py
- products.py
- conftest.py
Docker Setup
Section titled “Docker Setup”Dockerfile
Section titled “Dockerfile”FROM python:3.12-slim
WORKDIR /app
# Install dependenciesCOPY pyproject.toml poetry.lock ./RUN pip install poetry && \ poetry config virtualenvs.create false && \ poetry install --no-dev
# Copy applicationCOPY app ./app
# Run applicationCMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]docker-compose.yml
Section titled “docker-compose.yml”version: '3.8'
services: app: build: . ports: - "8000:8000" environment: DATABASE_URL: postgresql://zenith:zenith@db/zenith REDIS_URL: redis://redis:6379 depends_on: - db - redis volumes: - ./app:/app/app # For development
db: image: postgres:15 environment: POSTGRES_USER: zenith POSTGRES_PASSWORD: zenith POSTGRES_DB: zenith volumes: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432"
redis: image: redis:7-alpine ports: - "6379:6379"
volumes: postgres_data:Scaling Considerations
Section titled “Scaling Considerations”For Small Apps
Section titled “For Small Apps”For simple APIs, you can use a flatter structure:
Directorysimple-api/
- app.py Everything in one file
- models.py
- config.py
- tests.py
- requirements.txt
For Large Apps
Section titled “For Large Apps”For enterprise applications, add more layers:
Directoryenterprise-app/
Directorysrc/
Directorydomain/ Domain models
- …
Directoryapplication/ Use cases
- …
Directoryinfrastructure/ External services
- …
Directorypresentation/ API layer
- …
Directorytests/
- …
Directorydocs/
- …
Directorydeploy/
- …
Best Practices
Section titled “Best Practices”- Keep related code together - Group by feature, not by file type
- Use clear naming - Be explicit about what each module does
- Avoid circular imports - Use dependency injection
- Separate concerns - Business logic, data access, and web layer
- Configuration management - Use environment variables
- Test organization - Mirror your app structure
Next Steps
Section titled “Next Steps”- Learn about Services for organizing business logic
- Explore Configuration Management
- See Example Projects