Introduction
Austial is a batteries-included web framework for Python, built on top of FastAPI and inspired by NestJS . It gives Python decorator-driven modules/controllers/providers, real constructor-based dependency injection, the full request lifecycle (guards, pipes, interceptors, exception filters, middleware), a config layer, a database layer, Terminus-style health checks, a testing module — and a CLI so you can scaffold new projects and generate artifacts.
FastAPI is a fantastic ASGI toolkit, but it doesn’t prescribe an application architecture. Austial is opinionated on top of FastAPI: modules own controllers and providers, providers get dependency-injected by type, and the request pipeline
guards -> pipes -> interceptors (wrapping) -> handler -> exception filtersis a first-class concept instead of something you hand-roll with
Depends() everywhere.
Why Austial
Routing, guards, and interceptors are all declared with decorators that read top to bottom, in the order they execute:
from austial import Controller, Get, UseGuards, UseInterceptors
from austial.common.interceptors import TransformInterceptor
from .api_key_guard import ApiKeyGuard
@Controller("cats")
class CatsController:
@Get(":id")
@UseGuards(ApiKeyGuard)
@UseInterceptors(TransformInterceptor())
async def find_one(self, id: int):
...Path params use a familiar :id syntax (translated to FastAPI’s {id}
under the hood), so route strings stay short, and the decorator stack —
guard, interceptor, handler — reads top to bottom in the order it executes.
Architecture at a glance
- Modules —
@Module(imports=, controllers=, providers=, exports=)compose the application graph. - Controllers —
@Controller(prefix)plus@Get/@Post/@Put/@Patch/@Delete/@Options/@Headdeclare routes. - Providers —
@Injectable(scope=Scope.DEFAULT | Scope.TRANSIENT); the container resolves__init__type hints recursively, singleton by default. Custom providers use provider dicts:{"provide": TOKEN, "useClass"/"useValue"/"useFactory": ..., "inject": [...]}. - Bootstrap —
AustialFactory.create(AppModule)returns anAustialApplication;await app.listen(8000)starts it. - Guards —
CanActivateABC +ExecutionContext, applied with@UseGuards(...). - Interceptors —
NestInterceptorABC (intercept(context, call_next)), applied with@UseInterceptors(...). - Pipes —
PipeTransformABC, including the built-inValidationPipe. - Exception filters —
ExceptionFilterABC +@Catch(...), with a defaultAllExceptionsFilter. - Middleware —
NestMiddlewareABC, wired up viaconfigure(consumer)on modules. - Config —
ConfigModule.for_root(env_file=".env")/ConfigService. - Database —
DatabaseModule.for_root_async(use_factory=, inject=)over async SQLAlchemy. - Health checks —
austial.terminus.HealthCheckService+MemoryHealthIndicator,DatabaseHealthIndicator. - Testing —
austial.testing.Test.create_testing_module(...).compile(). - CLI —
austial new <name>/austial generate|g module|controller|service|resource <name>.
A note on file names
Some JavaScript frameworks name files with dots (health.controller.ts)
because Node resolves modules by explicit relative path strings. Python’s
import a.b instead resolves a as a package and b as a submodule — a
literal file health.controller.py isn’t importable via a normal import
statement. So Austial uses underscore suffixes instead: health_controller.py,
health_service.py, health_module.py, health_dto.py. The directory
layout is one folder per feature under src/modules/.
What’s not (yet) implemented
Austial is deliberately opinionated but not a 1:1 port of any single framework. A few concepts are intentionally out of scope for now, so you don’t go looking for them:
- Request-scoped providers (
Scope.REQUEST) — onlyScope.DEFAULT(singleton) andScope.TRANSIENTexist today. - Module
imports/exportsvisibility enforcement — every module’s providers currently register into one flat, application-wide container (visibility is documentation-only, not enforced at resolve time). - RxJS-style interceptor streams —
CallHandler.handle()is a plain async continuation, not an Observable.
None of this blocks building a real API with the full request lifecycle — it’s just worth knowing up front.