# How Air Uses Composition Over FastAPI Internally: Architecture and Implementation

> Discover how Air uses composition over FastAPI internally. Learn about its architecture and implementation, storing FastAPI within self._app for HTML-first applications while retaining full engine access.

- Repository: [Feldroy/air](https://github.com/feldroy/air)
- Tags: architecture
- Published: 2026-03-01

---

**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`](https://github.com/feldroy/air/blob/main/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:

```python
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`](https://github.com/feldroy/air/blob/main/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`:

```python

# 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`](https://github.com/feldroy/air/blob/main/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:

```python
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`](https://github.com/feldroy/air/blob/main/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:

```python

# 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:

```python
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`:

```python
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`](https://github.com/feldroy/air/blob/main/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:

```python
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`](https://github.com/feldroy/air/blob/main/src/air/applications.py) lines 65–78.
- Property proxies in [`src/air/applications.py`](https://github.com/feldroy/air/blob/main/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`](https://github.com/feldroy/air/blob/main/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`](https://github.com/feldroy/air/blob/main/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`](https://github.com/feldroy/air/blob/main/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.