# Recommended Project Structure for Air Applications: A Complete Guide

> Discover the recommended project structure for Air applications. Learn a minimal Python package layout for efficient routing, asset management, and dependency handling.

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

---

**The recommended project structure for Air applications follows a minimal Python package layout with [`main.py`](https://github.com/feldroy/air/blob/main/main.py) as the entry point, `routers/` for modular routes, `templates/` and `static/` for assets, and [`pyproject.toml`](https://github.com/feldroy/air/blob/main/pyproject.toml) for dependency management.**

Air applications are ordinary Python packages designed for rapid prototyping while maintaining flexibility to scale into larger systems. According to the feldroy/air repository, the framework embraces a conventional, minimal layout that supports everything from single-file demos to production-grade architectures with multiple routers and complex form handling.

## Core Principles of Air Project Layout

### Conventional Python Packaging

Air applications leverage standard Python packaging conventions rather than inventing new metaphors. The project uses [`pyproject.toml`](https://github.com/feldroy/air/blob/main/pyproject.toml) for metadata and dependency declaration, with the `air` CLI entry point defined in that file as shown in [`pyproject.toml`](https://github.com/feldroy/air/blob/main/pyproject.toml) lines 82-83. This ensures compatibility with modern Python tooling like `uv` and `pip`.

### Progressive Scalability

The documentation in [`docs/learn/quickstart.md`](https://github.com/feldroy/air/blob/main/docs/learn/quickstart.md) lines 7-16 describes a fast-start approach using `mkdir helloair && cd helloair && uv venv && source .venv/bin/activate && uv init && uv add air`. As complexity grows, the "Bigger Applications" cookbook in [`docs/learn/cookbook/bigger-applications.md`](https://github.com/feldroy/air/blob/main/docs/learn/cookbook/bigger-applications.md) lines 11-52 demonstrates how to evolve from a single [`main.py`](https://github.com/feldroy/air/blob/main/main.py) into a modular structure with separate router packages.

## Standard Directory Structure for Air Projects

The following layout represents the recommended project structure for Air applications, accommodating both quick prototypes and production deployments:

```

my-air-app/
├─ pyproject.toml          # Project metadata, Air as dependency

├─ main.py                 # Application entry point

├─ routers/                # Modular route groups

│   └─ dashboard.py        # AirRouter instances

├─ templates/              # Jinja2 or Air-Tag templates

│   └─ base.html
├─ static/                 # CSS, JS, images

│   ├─ styles.css
│   └─ scripts.js
├─ forms/                  # Pydantic-backed AirForm classes

│   └─ contact.py
└─ tests/                  # pytest suite

    └─ test_routes.py

```

**[`main.py`](https://github.com/feldroy/air/blob/main/main.py)** serves as the application factory, creating the `air.Air()` instance and mounting routers. The **`routers/`** directory contains `air.AirRouter` subclasses that group related pages, which you attach via `app.include_router()`. **`templates/`** holds Jinja2 templates (configured via `air.JinjaRenderer` as shown in [`docs/learn/quickstart.md`](https://github.com/feldroy/air/blob/main/docs/learn/quickstart.md) lines 390-410), while **`static/`** contains assets served via `app.mount("/static", air.StaticFiles(...))` per the static files cookbook in [`docs/learn/cookbook/static.md`](https://github.com/feldroy/air/blob/main/docs/learn/cookbook/static.md) lines 3-9.

## Implementing the Entry Point

The [`main.py`](https://github.com/feldroy/air/blob/main/main.py) file represents the minimal viable application. As demonstrated in [`docs/learn/quickstart.md`](https://github.com/feldroy/air/blob/main/docs/learn/quickstart.md) lines 34-45, this file instantiates the Air class and defines routes using the `@app.page` decorator:

```python

# main.py

import air

app = air.Air()
jinja = air.JinjaRenderer(directory="templates")  # Optional Jinja support

@app.page
def index():
    return air.layouts.mvpcss(
        air.Title("Home"),
        air.H1("Hello, Air!"),
        air.P("Breathe it in."),
    )

```

Run this application using the built-in CLI: `air run`.

## Modular Routing with AirRouter

For larger applications, split logic into separate modules using `air.AirRouter`. The router pattern shown in [`docs/learn/cookbook/bigger-applications.md`](https://github.com/feldroy/air/blob/main/docs/learn/cookbook/bigger-applications.md) allows you to define related pages in isolated files before mounting them on the main app:

```python

# routers/dashboard.py

import air

router = air.AirRouter()

@router.page
def dashboard():
    return air.layouts.mvpcss(
        air.Title("Dashboard"),
        air.H1("Welcome to the dashboard"),
    )

```

Then include the router in your entry point:

```python

# main.py

from routers.dashboard import router

app.include_router(router)  # Routes become /dashboard

```

## Template and Static Asset Organization

Air supports both its native **Air Tags** and **Jinja2** templating. When using Jinja, instantiate `air.JinjaRenderer(directory="templates")` and render templates by name:

```python
@app.page
def index(request: air.Request):
    return jinja(
        request,
        name="base.html",
        title="Home",
        fragment=air.H1("Dynamic content"),
    )

```

Expose static assets by mounting the directory:

```python
app.mount("/static", air.StaticFiles(directory="static"), name="static")

```

This makes files in `static/` accessible via [`/static/styles.css`](https://github.com/feldroy/air/blob/main//static/styles.css) or [`/static/scripts.js`](https://github.com/feldroy/air/blob/main//static/scripts.js) in your HTML.

## Form Handling and Validation

Keep form logic separate from views by storing `air.AirForm` classes in a dedicated `forms/` directory. As illustrated in the repository's examples, these forms use Pydantic models for validation:

```python

# forms/contact.py

import air
from pydantic import BaseModel, Field

class ContactModel(BaseModel):
    name: str = Field(min_length=2)
    email: str = Field(pattern=r'^[^@]+@[^@]+\.[^@]+$')

class ContactForm(air.AirForm):
    model = ContactModel

```

Import these into your views to render forms and handle validation:

```python
from forms.contact import ContactForm

@app.page
async def contact(request: air.Request):
    form = ContactForm()
    return air.layouts.mvpcss(
        air.Title("Contact Us"),
        form.render(),
    )

```

## Configuration and Dependencies

The [`pyproject.toml`](https://github.com/feldroy/air/blob/main/pyproject.toml) file declares Air as a dependency and configures the CLI entry point. While the Air repository itself places source code under `src/air/`, user applications typically place code in the repository root or follow the `src/` layout based on personal preference. The critical requirement is declaring `air` as a dependency to access the `air run`, `air fmt`, and other CLI commands.

## Testing Structure

Mirror the upstream repository's approach by placing tests under `tests/`. The feldroy/air repository includes comprehensive routing tests in [`tests/test_routing.py`](https://github.com/feldroy/air/blob/main/tests/test_routing.py), which you can use as a reference for writing unit tests against your own routes, forms, and background tasks.

## Summary

- **Start minimal** with a single [`main.py`](https://github.com/feldroy/air/blob/main/main.py) file containing an `air.Air()` instance, suitable for rapid prototyping.
- **Scale horizontally** by moving routes into `routers/` submodules using `air.AirRouter` and `app.include_router()`.
- **Separate concerns** by placing Jinja templates in `templates/`, static assets in `static/`, and form classes in `forms/`.
- **Use standard tooling** with [`pyproject.toml`](https://github.com/feldroy/air/blob/main/pyproject.toml) for dependencies and the `air` CLI for running and formatting code.
- **Test systematically** under `tests/` following the patterns established in the upstream test suite.

## Frequently Asked Questions

### Can I start with a single file before adopting the full directory structure?

Yes. The [`docs/learn/quickstart.md`](https://github.com/feldroy/air/blob/main/docs/learn/quickstart.md) explicitly recommends beginning with a single [`main.py`](https://github.com/feldroy/air/blob/main/main.py) file that creates an `Air` instance and defines routes directly. As your application grows, migrate routes to separate files in `routers/` without changing the core application logic.

### Where should I place Jinja templates in an Air project?

Place Jinja templates in a `templates/` directory at the project root. Instantiate `air.JinjaRenderer(directory="templates")` in your [`main.py`](https://github.com/feldroy/air/blob/main/main.py) file, then reference templates by name (e.g., `name="base.html"`) when returning responses from your page handlers.

### How do I organize static files like CSS and JavaScript?

Store static assets in a `static/` directory and mount it using `app.mount("/static", air.StaticFiles(directory="static"), name="static")`. This exposes the directory at the `/static/` URL path, allowing your templates to reference files like [`/static/styles.css`](https://github.com/feldroy/air/blob/main//static/styles.css).

### Does Air require a specific test runner or layout?

Air uses standard pytest for testing, with tests located in a `tests/` directory at the project root. The framework does not impose special testing requirements; you can write standard unit tests against your route functions and form classes using the patterns shown in [`tests/test_routing.py`](https://github.com/feldroy/air/blob/main/tests/test_routing.py) in the upstream repository.