Recommended Project Structure for Air Applications: A Complete Guide
The recommended project structure for Air applications follows a minimal Python package layout with main.py as the entry point, routers/ for modular routes, templates/ and static/ for assets, and 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 for metadata and dependency declaration, with the air CLI entry point defined in that file as shown in 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 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 lines 11-52 demonstrates how to evolve from a single 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 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 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 lines 3-9.
Implementing the Entry Point
The main.py file represents the minimal viable application. As demonstrated in docs/learn/quickstart.md lines 34-45, this file instantiates the Air class and defines routes using the @app.page decorator:
# 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 allows you to define related pages in isolated files before mounting them on the main app:
# 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:
# 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:
@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:
app.mount("/static", air.StaticFiles(directory="static"), name="static")
This makes files in static/ accessible via /static/styles.css or /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:
# 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:
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 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, 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.pyfile containing anair.Air()instance, suitable for rapid prototyping. - Scale horizontally by moving routes into
routers/submodules usingair.AirRouterandapp.include_router(). - Separate concerns by placing Jinja templates in
templates/, static assets instatic/, and form classes informs/. - Use standard tooling with
pyproject.tomlfor dependencies and theairCLI 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 explicitly recommends beginning with a single 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 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.
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 in the upstream repository.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →