Modules for Industrial and Fast Development

Flaxon modules are reusable application units built on the real router, dependency container, lifecycle hooks, static files, and Jinax templates. A module keeps one business area together while allowing the application to mount it at any prefix. This is useful for teams that need to ship quickly without turning app.py into a monolith.

When to use modules

Use one module for each domain such as accounts, orders, inventory, billing, or reporting. Keep repositories and services inside the domain module, expose routes from its module object, and mount all modules from one application entry point.

Reusable route packages

A module is a self-contained route package. Define its endpoints once, then attach the package to one or more applications or URL prefixes. The host application decides where the routes live; the module keeps its own handlers, dependencies, hooks, templates, and static assets.

# modules/catalog.py
from flaxon.modules import FlaxonModule

catalog = FlaxonModule("catalog")

@catalog.get("/products")
async def products(db):
    return await db.fetch_all("SELECT id, name, price FROM products")

@catalog.get("/products/")
async def product(product_id: int, db):
    return await db.fetch_one(
        "SELECT id, name, price FROM products WHERE id = ?", product_id
    )

# In app.py, mount the package wherever the application needs it.
from modules.catalog import catalog
app.mount_module(catalog, prefix="/store")
# Routes are now /store/products and /store/products/42.

This keeps feature code portable and makes a module easy to test in isolation. Mount the same package at /v1/store and /v2/store when an API needs versioned entry points, using a unique mount name for each registration.

1. Project structure

myapp/
|-- app.py
|-- pyproject.toml
|-- .env.example
|-- migrations/
|-- modules/
|   |-- __init__.py
|   |-- loader.py
|   |-- core/
|   |   `-- __init__.py
|   |-- accounts/
|   |   |-- __init__.py
|   |   |-- repository.py
|   |   |-- service.py
|   |   |-- templates/
|   |   `-- static/
|   `-- inventory/
|       |-- __init__.py
|       `-- repository.py
`-- tests/
    |-- test_accounts.py
    `-- test_inventory.py

This structure gives each team a clear ownership boundary. The application entry point wires shared infrastructure; modules own domain behavior.

2. Build a reusable module

# modules/inventory/__init__.py
from flaxon.modules import FlaxonModule
from flaxon.exceptions import NotFound

module = FlaxonModule("inventory")
module.requires("db")

@module.get("/")
async def list_inventory(db):
    return await db.fetch_all(
        "SELECT id, sku, name, stock FROM inventory ORDER BY name"
    )

@module.get("/")
async def get_item(item_id: int, db):
    item = await db.fetch_one(
        "SELECT id, sku, name, stock FROM inventory WHERE id = ?", item_id
    )
    if item is None:
        raise NotFound("Inventory item not found")
    return item

The module stays unaware of its final URL. Mount it at /api/inventory, /v1/inventory, or another prefix without changing the module code.

3. Mount modules with dependency injection

# app.py
from flaxon import Flaxon
from flaxon.database.manager import DatabaseManager
from flaxon.database.adapters.sqlite import SQLiteAdapter
from modules.inventory import module as inventory

app = Flaxon("shop", debug=True)
db = DatabaseManager(SQLiteAdapter(database="var/shop.sqlite3"))
app.container.register_instance("db", db)
app.mount_module(inventory, prefix="/api/inventory")

@app.on_startup
async def startup():
    await db.initialize()

@app.on_shutdown
async def shutdown():
    await db.close()

Register dependencies before mounting. If a module declares requires("db") and the container does not provide it, mounting fails early with a dependency error.

4. Mount the same module for API versions

from modules.inventory import module as inventory

app.mount_module(inventory, prefix="/v1/inventory", name="inventory-v1")
app.mount_module(inventory, prefix="/v2/inventory", name="inventory-v2")

Use a distinct name for repeated mounts. The same module instance can support versioned routes while the domain implementation remains shared.

5. Auto-discover modules

# modules/loader.py
import importlib
import pkgutil

def mount_all(app, prefix_base="/api"):
    package = importlib.import_module("modules")
    for _, name, is_package in pkgutil.iter_modules(package.__path__):
        if not is_package:
            continue
        current = importlib.import_module(f"modules.{name}")
        module = getattr(current, "module", None)
        if module is None:
            continue
        prefix = "" if name == "core" else f"{prefix_base}/{name}"
        app.mount_module(module, prefix=prefix)
# app.py
from modules.loader import mount_all

# Register db, mail, cache, and other shared services first.
mount_all(app, prefix_base="/api")

Adding a new domain becomes adding a package with a module-level module object. Keep startup dependencies registered before calling the loader.

6. Add module-scoped security

from flaxon.exceptions import Unauthorized
from flaxon.modules import FlaxonModule

staff = FlaxonModule("staff")

@staff.before_request
async def require_staff(request):
    if not request.session.get("staff_id"):
        raise Unauthorized("Staff login required")

@staff.after_request
async def record_access(request, result):
    request.app.logger.info("staff route: %s", request.path)

@staff.get("/reports")
async def reports(db):
    return await db.fetch_all("SELECT * FROM reports ORDER BY created_at DESC")

Module hooks only affect routes registered on that module. For Admin pages, also call the Admin permission guard inside each custom route and include CSRF validation for every browser mutation.

7. Add templates and static assets

from flaxon.http import Request
from flaxon.modules import FlaxonModule

portal = FlaxonModule(
    "portal",
    template_dir="modules/portal/templates",
    static_dir="modules/portal/static",
)

@portal.get("/")
async def home(request: Request):
    return await request.render("portal.html", {"title": "Staff portal"})

app.mount_module(portal, prefix="/portal")

Module static files are mounted under the module namespace. Existing app templates take precedence when names overlap, so an application can provide a controlled override without editing a dependency.

8. Industrial service boundaries

# modules/orders/service.py
class OrderService:
    def __init__(self, repository, payment_gateway):
        self.repository = repository
        self.payment_gateway = payment_gateway

    async def create(self, customer_id, lines):
        order = await self.repository.create_draft(customer_id, lines)
        await self.payment_gateway.authorize(order.total)
        return await self.repository.mark_authorized(order.id)

Route handlers should translate HTTP input and call services. Services own business rules, repositories own persistence, and workers own long-running operations. This makes the same behavior reusable from REST, GraphQL, Admin, WebSockets, and scheduled jobs.

9. Testing modules

import pytest
from flaxon.testing import AsyncTestClient

@pytest.mark.asyncio
async def test_inventory_list(app):
    async with AsyncTestClient(app) as client:
        response = await client.get("/api/inventory/")
    assert response.status_code == 200

@pytest.mark.asyncio
async def test_unknown_inventory_item(app):
    async with AsyncTestClient(app) as client:
        response = await client.get("/api/inventory/999999")
    assert response.status_code == 404

Test modules independently with fakes for fast feedback, then test the assembled application for mount prefixes, middleware, database lifecycle, authentication, and cross-module behavior.

10. Production checklist

  • Register shared database and service dependencies before module mounting.
  • Use migrations and close database connections during application shutdown.
  • Keep secrets in environment variables and disable debug in production.
  • Use Redis or a shared store for sessions, rate limits, locks, and events across workers.
  • Keep authorization in route and service boundaries, not only in navigation.
  • Use transactions for related writes and idempotency for retries.
  • Run flaxon routes, flaxon doctor, and the full test suite before release.
  • Run durable workers separately for scheduled jobs, thumbnails, mail, and retries.

Run it

pip install "flaxon[standard]"
flaxon migrate --database sqlite://./var/shop.sqlite3 --migrations-dir migrations
flaxon run app:app --reload --port 8000

For the complete framework behavior and edge cases, read the source lessons docs/guides/Modules.md and docs/lessons/production-app-guide.md in the Flaxon repository.