Structuring a project

Small apps fit in one file. Anything bigger deserves a structure where each module has one job. All the examples use this layout:

myapp/
├── README.md
├── app.py             # the composition root
├── settings.py        # settings, from the environment
├── models/            # database.py (engine + base) and one file per table group
├── services/          # one file per service: business rules (+ @remote API)
├── views/
│   ├── styles/        # theme and shared Style values
│   ├── components/    # reusable components
│   └── pages/         # pages, declared on Routers
└── tests/

Layers import across as absolute names (from views.components import Shell), and within a layer as relatives (from . import home).

Routers

A Router records pages, services and startup hooks without needing the app, so page modules never import app.py (which would be a circular import):

# views/pages/admin.py
from rational import Page, Router

router = Router(prefix="/admin")

@router.page("/")                  # served at /admin
class AdminHome(Page): ...

@router.page("/users/{user_id}")   # served at /admin/users/{user_id}
class AdminUser(Page):
    user_id: int

Routers nest with router.include(other); a views/pages/__init__.py typically combines them:

# views/pages/__init__.py
from rational import Router
from . import admin, public

router = Router()
router.include(public.router)
router.include(admin.router)

The composition root

app.py is the only module that knows how the parts fit together:

from models import create_tables, database
from services import AccountService, current_user
from views.pages import router

from rational import App

app = App(title="My app", database=database, login_path="/login")
app.authenticate(current_user)
app.include(router)
app.service(AccountService)       # expose its @remote methods under /api
app.on_startup(create_tables)

if __name__ == "__main__":
    app.run()

Run it from the project folder with python app.py --dev. Hot reload watches every Python file in the folder, including the packages.

Settings

Read settings once, from the environment, with safe defaults for development:

# settings.py
import os
from pathlib import Path

PROJECT_DIR = Path(__file__).resolve().parent
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite+aiosqlite:///{(PROJECT_DIR / 'app.db').as_posix()}")

Never commit secrets: read them from the environment in production.

Where code goes

settings.py

Constants and environment settings, read once at import time.

models/

database.py holds the engine and the declarative base; one file per table group, holding both the SQLAlchemy tables and the Pydantic shapes that cross the API and forms.

services/

Every rule about your data: queries, ownership checks, transactions, FieldError and RemoteError. Pages and the JSON API both call it, so the rules live in one place.

views/components/

Presentation and interaction. Components call services through self.use(Service).

views/pages/

Routing, loading data in mount(), and composing components.

views/styles/

The theme preset and shared Style values, so look decisions have one home.