Skip to content

Project 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

The main application file where you create and configure your Zenith app:

app/main.py
from zenith import Zenith
from app.config import settings
from app.routes import auth, users, products, orders
from app.middleware import setup_middleware
# Create application
app = Zenith(
title=settings.APP_NAME,
version=settings.APP_VERSION,
debug=settings.DEBUG
)
# Setup middleware
setup_middleware(app)
# Include routers
app.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)

Centralized configuration using environment variables:

app/config.py
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings
from 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 instance
settings = Settings()

Business logic organized by domain:

app/services/users.py
from zenith import Service, Inject
from app.models.user import User, UserCreate, UserUpdate
from app.external.email import EmailService
from 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()

Data models using SQLModel:

app/models/user.py
from zenith.db import SQLModel, Field
from typing import Optional
from 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: datetime

API endpoints organized by resource:

app/routes/users.py
from zenith import Router, Inject
from app.services.users import UserService
from app.models.user import User, UserCreate, UserUpdate, UserResponse
from app.auth import get_current_user
from 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)
Terminal window
# Application
APP_NAME="Zenith Dev"
DEBUG=true
ENVIRONMENT=development
# Database
DATABASE_URL=postgresql://zenith:zenith@localhost/zenith_dev
# Redis
REDIS_URL=redis://localhost:6379
# Security
SECRET_KEY=your-secret-key-for-development
# Email (using MailHog for dev)
SMTP_HOST=localhost
SMTP_PORT=1025
Terminal window
# Application
APP_NAME="Zenith API"
DEBUG=false
ENVIRONMENT=production
# Database
DATABASE_URL=postgresql://user:pass@db.example.com/zenith_prod
# Redis
REDIS_URL=redis://redis.example.com:6379
# Security
SECRET_KEY=${SECRET_KEY} # From environment
# Email
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASSWORD=${SENDGRID_API_KEY}
# AWS
AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
S3_BUCKET=zenith-uploads

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
FROM python:3.12-slim
WORKDIR /app
# Install dependencies
COPY pyproject.toml poetry.lock ./
RUN pip install poetry && \
poetry config virtualenvs.create false && \
poetry install --no-dev
# Copy application
COPY app ./app
# Run application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
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:

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 enterprise applications, add more layers:

  • Directoryenterprise-app/
    • Directorysrc/
      • Directorydomain/ Domain models
      • Directoryapplication/ Use cases
      • Directoryinfrastructure/ External services
      • Directorypresentation/ API layer
    • Directorytests/
    • Directorydocs/
    • Directorydeploy/
  1. Keep related code together - Group by feature, not by file type
  2. Use clear naming - Be explicit about what each module does
  3. Avoid circular imports - Use dependency injection
  4. Separate concerns - Business logic, data access, and web layer
  5. Configuration management - Use environment variables
  6. Test organization - Mirror your app structure