How Air Uses Composition Over FastAPI Internally: Architecture and Implementation

Air wraps FastAPI through composition rather than inheritance, storing the framework instance in self._app and proxying only the functionality needed for HTML-first applications while maintaining full access to the underlying FastAPI engine.

Air, the HTML-first Python web framework from feldroy/air, avoids subclassing FastAPI by instead composing a FastAPI (or APIRouter) instance internally. This design pattern allows Air to inject HTML-centric features like custom response classes and route handlers without modifying the underlying framework's behavior. By holding the framework object as an attribute and forwarding select calls, Air creates a clean separation between its HTML utilities and FastAPI's core API machinery.

Application Composition in the Air Class

The Air class in src/air/applications.py never inherits from FastAPI. Instead, it accepts an optional fastapi_app parameter or creates an internal instance, storing it in self._app to enable precise control over HTML rendering behavior.

Internal FastAPI Instance Management

During initialization, Air.__init__ checks for an existing FastAPI application. If none is provided, it instantiates a new one with forced defaults for HTML output:

class Air(RouterMixin):
    def __init__(self, ..., fastapi_app: FastAPI | None = None, ...):
        if fastapi_app is None:
            self._app = FastAPI(
                debug=debug,
                routes=routes,
                default_response_class=AirResponse,  # Custom HTML response

                route_class=AirRoute,                  # Custom route handling

                ...
            )
        else:
            self._app = fastapi_app  # User-provided instance

As implemented in lines 65–78 of src/air/applications.py, this composition pattern allows Air to force AirResponse as the default response class and AirRoute as the route class while passing all other configuration parameters through unchanged.

Property Proxies and ASGI Delegation

Rather than reimplementing FastAPI's public interface, Air exposes attributes through property proxies that forward to the composed instance. The ASGI callable also delegates directly to self._app:


# Property proxy example (lines 20–24)

@property
def state(self) -> Any:
    return self._app.state

# ASGI delegation (lines 11–13)

async def __call__(self, scope, receive, send):
    await self._app(scope, receive, send)

This forwarding mechanism ensures that app.state, app.router, and other FastAPI attributes remain accessible while Air maintains its own HTML-focused API surface.

Router Composition with AirRouter

Air extends its composition pattern to routing through AirRouter, which wraps Starlette's APIRouter rather than inheriting from it. This design resides primarily in src/air/routing.py.

Composing APIRouter Instances

The AirRouter class stores an internal APIRouter instance in self._router, forcing the use of AirRoute for all endpoints:

class AirRouter(RouterMixin):
    def __init__(self, ..., route_class: type[AirRoute] = AirRoute, ...):
        self._router = APIRouter(
            prefix=prefix,
            tags=tags,
            route_class=route_class,  # Forces AirRoute

            ...
        )

According to lines 445–452 in src/air/routing.py, this composition ensures every route registered through an AirRouter receives the custom AirRequest object and HTML-specific handling without modifying the underlying router's core logic.

Unified Routing Interface via RouterMixin

Both Air and AirRouter inherit from RouterMixin, which provides the _target property to identify the composed object. For AirRouter, _target returns self._router; for Air, it returns self._app. All routing decorators (get, post, put, etc.) delegate to this target via the _route method:


# Routing delegation (lines 92–102)

def get(self, path, **kwargs):
    return self._route(path, methods=["GET"], **kwargs)

This abstraction allows both application and router objects to use identical decorator syntax while internally forwarding calls to their respective composed FastAPI components.

Why Composition Matters for HTML-First Development

The composition over inheritance pattern in Air provides three critical advantages for HTML-centric web development:

  • Isolation of concerns – Air's HTML utilities (AirResponse, AirRequest, AirRoute) live in dedicated classes separate from FastAPI's JSON-centric defaults. The underlying engine remains untouched and swappable.
  • Flexible extension – Users can access the raw FastAPI instance through app.fastapi_app to add middleware, customize OpenAPI schemas, or implement WebSockets without breaking Air's higher-level API.
  • Testability – Because Air forwards calls to composed objects, unit tests can inject mock FastAPI instances into self._app or self._router, isolating Air's logic from the framework's request handling.

Practical Implementation Examples

Basic Application with Internal Composition

Create an Air application that automatically composes a FastAPI instance internally:

import air

app = air.Air(debug=True)

@app.get("/")
def index() -> air.H1:
    return air.H1("Welcome to Air!")

# Access underlying FastAPI for advanced configuration

fastapi = app.fastapi_app
fastapi.add_middleware(SomeMiddleware)

The app object holds the FastAPI instance in app._app, exposing all standard FastAPI features through the fastapi_app property while providing Air's HTML tag helpers.

Composed Routers for Modular Applications

Use AirRouter to compose modular routing components that internally wrap APIRouter:

router = air.AirRouter(prefix="/admin")

@router.get("/dashboard")
def dashboard() -> air.Div:
    return air.Div("Admin dashboard")

app.include_router(router)  # Delegates to internal APIRouter

As shown in lines 84–87 of src/air/applications.py, include_router extracts the underlying router from the composed AirRouter instance and forwards the call to FastAPI's native router inclusion logic.

Injecting a Pre-Configured FastAPI Instance

Override the default composition by supplying your own FastAPI instance:

from fastapi import FastAPI
import air

fastapi = FastAPI(default_response_class=air.AirResponse)
app = air.Air(fastapi_app=fastapi)

This pattern stores the user-provided instance directly in app._app, preserving custom middleware stacks and configuration while enabling Air's HTML response handling.

Summary

  • Air stores the FastAPI instance in self._app rather than inheriting from the FastAPI class, as defined in src/air/applications.py lines 65–78.
  • Property proxies in src/air/applications.py (lines 20–24) forward public attributes like state and router to the composed instance.
  • AirRouter composes an internal APIRouter in self._router with route_class forced to AirRoute, implemented in src/air/routing.py lines 445–452.
  • The ASGI interface delegates request handling directly to the underlying FastAPI application via await self._app(scope, receive, send).
  • Users can inject pre-configured FastAPI instances via the fastapi_app parameter to customize middleware and OpenAPI generation while retaining Air's HTML-first features.

Frequently Asked Questions

Does Air inherit from FastAPI?

No. According to the source code in src/air/applications.py, the Air class stores the FastAPI instance in self._app and uses property proxies to expose necessary attributes. This composition pattern allows Air to remain framework-agnostic at the inheritance level while leveraging FastAPI's capabilities.

How does Air handle HTTP routing without subclassing FastAPI?

Air routes HTTP requests through RouterMixin, which provides a _target property returning the composed object. For AirRouter, this is self._router (an APIRouter instance). Routing decorators like get() and post() delegate to this target via the _route method, as shown in src/air/routing.py lines 92–102.

Can I access the underlying FastAPI application for advanced customization?

Yes. Air exposes the composed FastAPI instance through the fastapi_app property. This allows direct access to add_middleware(), openapi() customization, WebSocket endpoints, and other FastAPI-native features without breaking Air's HTML-first abstraction layer.

What customizations does Air force on the composed FastAPI instance?

Air forces two critical defaults during composition: it sets default_response_class to AirResponse and route_class to AirRoute. These ensure HTML-centric request handling and custom AirRequest objects. All other FastAPI configuration parameters pass through unchanged to the constructor.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →