Project Structure¶
As your Cello application grows beyond a single file, organizing code into a clear structure makes it easier to maintain, test, and scale. This guide covers recommended layouts from small projects to large multi-module applications.
Small Project¶
For simple APIs and prototypes, a flat structure works well:
my-app/
app.py # Application entry point
requirements.txt # Python dependencies
.env # Environment variables (not in git)
.gitignore
# app.py
from cello import App
app = App()
@app.get("/")
def home(request):
return {"message": "Hello!"}
if __name__ == "__main__":
app.run()
Medium Project¶
When you have multiple resources, use blueprints to split routes into separate modules:
my-app/
app.py # Application entry point
config.py # Configuration
routes/
__init__.py
users.py # User routes
products.py # Product routes
health.py # Health check routes
services/
__init__.py
user_service.py # Business logic
product_service.py
tests/
__init__.py
test_users.py
test_products.py
requirements.txt
.env
Entry Point (app.py)¶
from cello import App
from config import configure_app
from routes.users import users_bp
from routes.products import products_bp
from routes.health import health_bp
app = App()
configure_app(app)
# Register blueprints
app.register_blueprint(users_bp)
app.register_blueprint(products_bp)
app.register_blueprint(health_bp)
if __name__ == "__main__":
app.run()
Configuration (config.py)¶
import os
class Config:
DEBUG = os.getenv("DEBUG", "false") == "true"
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite://app.db")
SECRET_KEY = os.getenv("SECRET_KEY", "change-me-in-production")
CORS_ORIGINS = os.getenv("CORS_ORIGINS", "*").split(",")
def configure_app(app):
"""Apply configuration to the app."""
config = Config()
app.enable_cors(config.CORS_ORIGINS)
app.enable_logging()
app.enable_compression()
app.register_singleton("config", config)
Route Module (routes/users.py)¶
from cello import Blueprint, Response, Depends
users_bp = Blueprint("/api/users")
@users_bp.get("/")
def list_users(request):
"""List all users."""
return {"users": []}
@users_bp.get("/{id}")
def get_user(request):
"""Get a user by ID."""
user_id = request.params["id"]
return {"id": user_id, "name": f"User {user_id}"}
@users_bp.post("/")
def create_user(request):
"""Create a new user."""
data = request.json()
return Response.json({"id": 1, **data}, status=201)
@users_bp.delete("/{id}")
def delete_user(request):
"""Delete a user."""
return {"deleted": True}
Large Project¶
For full-scale applications with multiple domains, middleware, and infrastructure concerns:
my-app/
app.py # Application entry point
config/
__init__.py
settings.py # Environment-based settings
security.py # Security configuration
database.py # Database configuration
api/
__init__.py
v1/
__init__.py
users/
__init__.py
routes.py # Route definitions
service.py # Business logic
models.py # Data models / DTOs
products/
__init__.py
routes.py
service.py
models.py
orders/
__init__.py
routes.py
service.py
models.py
v2/
__init__.py
users/
routes.py # V2 user routes
middleware/
__init__.py
auth.py # Custom auth middleware
tenant.py # Multi-tenant middleware
templates/
emails/
welcome.html
reset_password.html
pages/
index.html
static/
css/
js/
images/
tests/
__init__.py
conftest.py # Shared test fixtures
api/
test_users.py
test_products.py
test_orders.py
integration/
test_auth_flow.py
scripts/
seed_db.py # Database seeding
migrate.py # Migrations
Dockerfile
docker-compose.yml
requirements.txt
pyproject.toml
.env
.env.example
API Versioning with Blueprints¶
# api/v1/__init__.py
from cello import Blueprint
from .users.routes import users_bp
from .products.routes import products_bp
from .orders.routes import orders_bp
v1 = Blueprint("/api/v1")
v1.register(users_bp)
v1.register(products_bp)
v1.register(orders_bp)
# api/v2/__init__.py
from cello import Blueprint
from .users.routes import users_bp
v2 = Blueprint("/api/v2")
v2.register(users_bp)
# app.py
from cello import App
from api.v1 import v1
from api.v2 import v2
app = App()
app.register_blueprint(v1)
app.register_blueprint(v2)
# Routes:
# /api/v1/users, /api/v1/products, /api/v1/orders
# /api/v2/users
Separating Routes into Blueprints¶
By Resource¶
Group all routes for a resource in a single blueprint:
# api/v1/users/routes.py
from cello import Blueprint, Response
users_bp = Blueprint("/users")
@users_bp.get("/")
def list_users(request):
return {"users": []}
@users_bp.get("/{id}")
def get_user(request):
return {"id": request.params["id"]}
@users_bp.post("/")
def create_user(request):
return Response.json(request.json(), status=201)
By Function¶
Separate public and admin routes:
# Public routes (no auth required)
public_bp = Blueprint("/public")
@public_bp.get("/status")
def status(request):
return {"status": "ok"}
# Admin routes (auth required)
admin_bp = Blueprint("/admin")
@admin_bp.get("/dashboard")
def dashboard(request):
return {"admin": True}
# Register both
app.register_blueprint(public_bp)
app.register_blueprint(admin_bp)
Tests Directory¶
Organize tests to mirror your application structure:
tests/
__init__.py
conftest.py # Shared fixtures
test_health.py # Health check tests
api/
__init__.py
test_users.py # User endpoint tests
test_products.py # Product endpoint tests
integration/
test_full_flow.py # End-to-end tests
Example Test¶
# tests/api/test_users.py
import requests
BASE_URL = "http://127.0.0.1:8000"
def test_list_users():
response = requests.get(f"{BASE_URL}/api/users")
assert response.status_code == 200
data = response.json()
assert "users" in data
def test_create_user():
response = requests.post(
f"{BASE_URL}/api/users",
json={"name": "Alice", "email": "alice@example.com"},
)
assert response.status_code == 201
assert response.json()["name"] == "Alice"
Configuration Files¶
Environment Variables (.env)¶
# .env
DEBUG=true
DATABASE_URL=postgres://localhost/mydb
SECRET_KEY=your-secret-key-minimum-32-bytes
CORS_ORIGINS=http://localhost:3000,http://localhost:8080
WORKERS=4
.env.example¶
Provide a template without secrets:
# .env.example
DEBUG=false
DATABASE_URL=postgres://localhost/mydb
SECRET_KEY=change-me
CORS_ORIGINS=*
WORKERS=4
.gitignore¶
Best Practices¶
Recommendations
- One blueprint per resource -- keeps related routes together.
- Separate business logic from routes -- put complex logic in service modules.
- Use
__init__.pyto re-export blueprints for cleaner imports. - Keep
app.pythin -- it should only wire things together. - Store secrets in environment variables, never in code.
- Mirror app structure in tests for easy navigation.
Next Steps¶
- Configuration - App configuration options
- Routing - Blueprint and route details
- First App - Step-by-step tutorial