Skip to course content
Free FastAPI backend course

FastAPI for AI Backend Development

Unit 07.01: Settings and clients that are built once

Expensive objects should be built once and injected, not created per request.

A cached dependency is a singleton

Settings behind a cache, used by an endpoint called three times.

The code counts how many times it was built.

from functools import lru_cache
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from pydantic_settings import BaseSettings

BUILDS = []


class Settings(BaseSettings):
    model_name: str = "fake-model-for-tests"


@lru_cache
def get_settings() -> Settings:
    BUILDS.append(1)
    return Settings()


app = FastAPI()


@app.get("/config")
def config(settings: Settings = Depends(get_settings)) -> dict:
    return {"model": settings.model_name}


client = TestClient(app)
for _ in range(3):
    client.get("/config")
print(f"three requests, settings built {len(BUILDS)} time(s)")

print("\n`lru_cache` is what makes this a singleton. The same pattern suits any")
print("expensive client -- a database pool, an HTTP session, a loaded model.")

Three requests, one construction. The cache is what makes it a singleton, and the same pattern suits any expensive object: a connection pool, an HTTP session, a loaded model.

Injecting rather than importing is what makes it replaceable in a test - which is the next unit but one, and the reason the whole suite can run without a real model.

The mistake this prevents

The mistake is a module-level global created at import time. It cannot be overridden in a test, it is built even when unused, and importing the module becomes a side effect - sometimes a network call.

Takeaway

Build expensive objects once behind a cached dependency and inject them. Module-level globals cannot be replaced in a test.