Skip to content

Best PracticesΒΆ

This guide collects practical recommendations for building reliable, maintainable, and performant Cello applications.


Project StructureΒΆ

Organize code by feature rather than by layer. A clean layout for a medium-sized project:

myproject/
β”œβ”€β”€ app.py                # Application entry point
β”œβ”€β”€ config.py             # Configuration & environment
β”œβ”€β”€ blueprints/
β”‚   β”œβ”€β”€ users.py          # User routes
β”‚   β”œβ”€β”€ orders.py         # Order routes
β”‚   └── admin.py          # Admin routes
β”œβ”€β”€ services/
β”‚   β”œβ”€β”€ user_service.py   # Business logic
β”‚   └── order_service.py
β”œβ”€β”€ repositories/
β”‚   β”œβ”€β”€ user_repo.py      # Data access
β”‚   └── order_repo.py
β”œβ”€β”€ middleware/
β”‚   └── auth.py           # Custom middleware
β”œβ”€β”€ guards/
β”‚   └── permissions.py    # Custom guards
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ test_users.py
β”‚   └── test_orders.py
└── requirements.txt

Tip

Use Blueprints to map each module in blueprints/ to a URL prefix. Register them all in app.py.


Error Handling PatternsΒΆ

Always return structured errorsΒΆ

Return RFC 7807-style problem details instead of plain strings.

@app.exception_handler(ValueError)
def handle_value_error(request, exc):
    return Response.json({
        "type": "/errors/validation",
        "title": "Validation Error",
        "status": 400,
        "detail": str(exc),
    }, status=400)

Fail fast on invalid inputΒΆ

Validate early in handlers. Return 400 immediately rather than letting bad data propagate.

@app.post("/users")
def create_user(request):
    data = request.json()
    if not data.get("email"):
        return Response.json({"error": "email is required"}, status=400)
    # ... proceed with valid data

Async Best PracticesΒΆ

Use async handlers for I/OΒΆ

When a handler calls a database, an external API, or reads a file, use async def.

@app.get("/users")
async def list_users(request):
    users = await db.fetch_all("SELECT * FROM users")
    return {"users": users}

Never block the event loopΒΆ

Avoid calling time.sleep(), synchronous requests.get(), or open() inside an async handler. Use their async equivalents or offload to a thread.

Keep handlers thinΒΆ

Handlers should parse input, call a service, and return the result. Business logic belongs in a service layer.

@app.post("/orders")
async def create_order(request):
    data = request.json()
    order = await order_service.create(data)
    return Response.json(order, status=201)

Security ChecklistΒΆ

Area Action
Secrets Load from environment variables, never hard-code
HTTPS Enable TlsConfig in production
CORS Restrict origins to your actual domains
Rate limiting Enable on authentication and public endpoints
Headers Enable SecurityHeadersConfig for CSP, HSTS, X-Frame-Options
JWT Use short-lived access tokens (15-60 min) with refresh tokens
Input Validate all user input; use DTO validation or Pydantic models
Dependencies Run pip audit and cargo audit regularly

Performance TipsΒΆ

Return dicts, not Response objectsΒΆ

Returning a plain dict lets Cello serialize JSON entirely in Rust via SIMD. Creating a Response object is only necessary when you need a custom status code or headers.

Use path parameters over query parametersΒΆ

Path parameters are resolved during routing in the Rust radix tree and are faster to access than query strings parsed at runtime.

Enable compressionΒΆ

For responses larger than 1 KB, enable gzip compression to reduce bandwidth.

app.enable_compression(min_size=1024)

Use cachingΒΆ

Apply the @cache decorator to expensive read-only endpoints.

from cello import cache

@app.get("/reports/daily")
@cache(ttl=600, tags=["reports"])
def daily_report(request):
    return generate_report()

Testing StrategiesΒΆ

Test routes in isolationΒΆ

Use pytest with the requests library against a running instance, or mock the Cello request object.

Use fixtures for the appΒΆ

Create a conftest.py fixture that starts the server once per session.

Test error pathsΒΆ

Every handler should have tests for both the happy path and expected error conditions (400, 404, 401, etc.).

Separate unit and integration testsΒΆ

  • Unit tests: Test services and repositories with mocked dependencies.
  • Integration tests: Test full HTTP round-trips including middleware.

Logging StandardsΒΆ

Enable structured logging in productionΒΆ

app.enable_logging()

Cello logs request method, path, status code, and latency automatically. Add context to your own log messages:

import logging
logger = logging.getLogger(__name__)

@app.post("/orders")
def create_order(request):
    logger.info("Creating order", extra={"user_id": request.context.get("user", {}).get("sub")})
    # ...

Use request IDs for tracingΒΆ

Enable the request ID middleware to correlate logs across a single request.

app.enable_request_id()

Each response will include an X-Request-ID header that you can use in log filters.


Configuration ManagementΒΆ

Use environment-based configurationΒΆ

import os

class Config:
    DEBUG = os.environ.get("CELLO_DEBUG", "false").lower() == "true"
    DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite://dev.db")
    JWT_SECRET = os.environ.get("JWT_SECRET", "change-me")
    WORKERS = int(os.environ.get("WORKERS", "4"))

Separate dev and production settingsΒΆ

Pass --env production on the command line or set CELLO_ENV=production. In production, Cello disables debug mode and verbose logging by default.


SummaryΒΆ

Do Avoid
Return dict from handlers Creating Response objects unnecessarily
Use async def for I/O operations Blocking calls in async handlers
Validate input early Letting bad data reach business logic
Use Blueprints for route organization Defining all routes in a single file
Load secrets from environment Hard-coding credentials
Enable security middleware Leaving CORS open to * in production
Write tests for error paths Testing only the happy path