Project Structure¶
When you run paxx bootstrap myproject, the following structure is created:
myproject/
├── main.py # Application entry point & factory
├── settings.py # Configuration (Pydantic Settings)
├── conftest.py # Root pytest fixtures
├── alembic.ini # Alembic configuration
├── pyproject.toml # Project dependencies
├── Makefile # Common development commands
├── .env # Environment variables (git-ignored)
├── .env.example # Example environment variables
├── .gitignore # Git ignore rules
├── README.md # Project readme
├── DEPLOY.md # Deployment documentation
│
├── Dockerfile # Production container image
├── Dockerfile.dev # Development container image
├── docker-compose.yml # Development environment
├── .dockerignore # Docker build exclusions
│
├── core/ # Core utilities
│ ├── __init__.py
│ ├── logging.py # Structured logging configuration
│ ├── exceptions.py # Custom exceptions & handlers
│ ├── middleware.py # Custom middleware
│ ├── dependencies.py # FastAPI dependencies
│ └── schemas.py # Shared Pydantic schemas
│
├── db/ # Database
│ ├── __init__.py
│ ├── database.py # Async SQLAlchemy setup
│ └── migrations/ # Alembic migrations
│ ├── env.py # Alembic environment
│ ├── script.py.mako # Migration template
│ └── versions/ # Migration files
│
├── features/ # Domain features
│ └── health/ # Built-in health check
│ ├── __init__.py
│ └── routes.py
│
├── e2e/ # End-to-end tests
│ ├── __init__.py
│ ├── conftest.py # Test fixtures
│ └── test_health.py # Health endpoint tests
│
└── deploy/ # Deployment configs
└── README.md # Instructions for adding deployments
This structure is a starting point, not a constraint. Feel free to reorganize directories, rename modules, or reshape the architecture to fit your project's needs. After bootstrapping, the code is entirely yours.
Key Files¶
main.py¶
The application entry point using the factory pattern with async lifespan:
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: validate database connection
if not await verify_database_connection():
sys.exit(1)
yield
# Shutdown: close connections
await close_db()
def create_app() -> FastAPI:
app = FastAPI(
title=settings.app_name,
debug=settings.debug,
lifespan=lifespan,
)
# Register exception handlers
register_exception_handlers(app)
# Register middleware
register_middleware(app)
# Configure CORS
app.add_middleware(CORSMiddleware, ...)
# Register routers
app.include_router(health_router, tags=["health"])
return app
app = create_app()
settings.py¶
Type-safe configuration using Pydantic Settings:
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
case_sensitive=False,
)
# Application
app_name: str = "myproject"
debug: bool = False
environment: Literal["development", "staging", "production"] = "development"
# Logging
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
log_format: Literal["json", "console"] = "console"
# Server
host: str = "127.0.0.1"
port: int = 8000
# Database
database_url: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/myproject"
# Security
secret_key: str = "CHANGE-ME-IN-PRODUCTION-USE-SECRETS-TOKEN"
access_token_expire_minutes: int = 30
# CORS
cors_origins: list[str] = ["http://localhost:3000"]
settings = Settings()
db/database.py¶
Async SQLAlchemy setup with session management:
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, DeclarativeBase
engine = create_async_engine(settings.database_url)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
async def get_db():
async with AsyncSessionLocal() as session:
yield session
core/logging.py¶
Structured logging with JSON and console output:
from core.logging import configure_logging, get_logger
configure_logging(level="INFO", format="json")
logger = get_logger(__name__)
logger.info("Request processed", user_id=123, status="success")
core/exceptions.py¶
Custom exception classes and handlers:
from core.exceptions import NotFoundError, ValidationError
# Raise custom exceptions in your code
raise NotFoundError("User not found")
raise ValidationError("Invalid email format")
core/dependencies.py¶
FastAPI dependencies for common patterns:
from core.dependencies import get_db, PaginationParams
@router.get("/items")
async def list_items(
db: AsyncSession = Depends(get_db),
pagination: PaginationParams = Depends(),
):
...
Feature Structure¶
Each feature in features/ follows this structure:
features/<name>/
├── __init__.py # Feature exports
├── config.py # Router configuration
├── models.py # SQLAlchemy models
├── schemas.py # Pydantic schemas
├── services.py # Business logic
└── routes.py # API endpoints
config.py¶
Defines the router prefix and OpenAPI tags:
from dataclasses import dataclass, field
@dataclass
class FeatureConfig:
prefix: str = "/users"
tags: list[str] = field(default_factory=lambda: ["Users"])
models.py¶
SQLAlchemy models using modern mapped column syntax:
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from db.database import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True)
name: Mapped[str | None] = mapped_column(String(100))
schemas.py¶
Pydantic schemas for request/response validation:
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
email: EmailStr
name: str | None = None
class UserResponse(BaseModel):
id: int
email: str
name: str | None
model_config = {"from_attributes": True}
services.py¶
Business logic layer with async functions:
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from .models import User
from .schemas import UserCreate
async def create_user(db: AsyncSession, data: UserCreate) -> User:
user = User(**data.model_dump())
db.add(user)
await db.commit()
await db.refresh(user)
return user
async def get_user(db: AsyncSession, user_id: int) -> User | None:
result = await db.execute(select(User).where(User.id == user_id))
return result.scalar_one_or_none()
routes.py¶
FastAPI router with endpoints delegating to services:
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from core.dependencies import get_db
from . import services, schemas
router = APIRouter()
@router.post("/", response_model=schemas.UserResponse, status_code=201)
async def create_user(
data: schemas.UserCreate,
db: AsyncSession = Depends(get_db),
):
return await services.create_user(db, data)
@router.get("/{user_id}", response_model=schemas.UserResponse)
async def get_user(
user_id: int,
db: AsyncSession = Depends(get_db),
):
user = await services.get_user(db, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
Directory Conventions¶
| Directory | Purpose |
|---|---|
core/ |
Shared utilities, middleware, dependencies, schemas |
db/ |
Database configuration, models base, migrations |
features/ |
Domain features (business logic organized by capability) |
e2e/ |
End-to-end API tests |
deploy/ |
Deployment configurations (added via paxx deploy add) |
Registering Features¶
Manual Registration¶
After creating a feature with paxx feature create, register it in main.py:
from features.users.routes import router as users_router
from features.users.config import FeatureConfig as UsersConfig
def create_app() -> FastAPI:
app = FastAPI()
users_config = UsersConfig()
app.include_router(
users_router,
prefix=users_config.prefix,
tags=users_config.tags,
)
return app
Automatic Registration¶
When using paxx feature add with bundled features, the router is automatically registered in main.py using AST parsing.
Environment Variables¶
Key environment variables configured in .env:
| Variable | Description | Default |
|---|---|---|
APP_NAME |
Application name | Project name |
DEBUG |
Enable debug mode | false |
ENVIRONMENT |
Environment type | development |
LOG_LEVEL |
Logging level | INFO |
LOG_FORMAT |
Log format (json/console) | console |
DATABASE_URL |
PostgreSQL connection string | Local postgres |
SECRET_KEY |
JWT signing key | Must change in production |
CORS_ORIGINS |
Allowed CORS origins | ["http://localhost:3000"] |
Next Steps¶
Follow the Tutorial to build a complete feature.
If you already know how to build features, you can skip the tutorial and add the pre-built example:
This adds a complete products feature with:
- Full CRUD implementation
- E2E tests included
- Automatic router registration