~/work/bustan

Bustan — NestJS-inspired API framework for Python

Modules, providers and constructor injection on ASGI, so the dependency structure of a service is something you can read rather than reconstruct.

Bustan (بستان — garden) brings NestJS's architecture to Python: modules that declare what they need and what they expose, providers resolved by a container, and controllers that stay thin.

@Injectable()
class TaskService: ...

@Controller("/tasks")
class TaskController:
    def __init__(self, tasks: TaskService) -> None:
        self.tasks = tasks

@Module(controllers=[TaskController], providers=[TaskService])
class TaskModule:
    pass

TaskService is injected because the constructor asks for it by type. Nothing else to register.

Why#

Python has excellent routers. What it lacks is a convention for structure that survives a codebase growing past one file — where a newcomer can tell, from the module declarations alone, what depends on what.

Per-handler dependency injection, the usual Python answer, is genuinely good for small services. It just leaves the dependency graph implicit, scattered across every route signature.

Visibility is the design#

A provider is private to its module unless exported, and reachable only by modules that import the owner. Two lists, and the whole graph falls out of them:

$ bustan graph
AppModule
├── TaskModule
│   ├── provides  TaskService
│   └── exports   TaskService
└── ReportModule
    ├── imports   TaskModule
    └── provides  ReportService

Everything-global is more convenient for about three weeks; after that "what breaks if I change this" has no answer short of running everything.

Resolution is eager#

The graph is walked at startup, providers are constructed in dependency order, and cycles are errors before the first request. A framework that resolves lazily turns a structural mistake into an intermittent one.

The same strictness rejects a request-scoped provider injected into a singleton — which would otherwise capture the first request's instance forever and surface weeks later as one user seeing another's data.

Trade-offs#

Ceremony. A three-endpoint service is more code here than in a bare router, and for that shape of problem the bare router is the better tool. The trade pays off when a second person joins or a module has to move — and if the codebase never gets there, you simply paid.

Docs are in the Bustan app.