Helper function to register multiple routers at once, reducing boilerplate.
register_routes
def register_routes( app, # FastHTML app instance routers:VAR_POSITIONAL, # One or more APIRouter instances to register)->None: # No return value
Register multiple APIRouter instances to a FastHTML app at once.
Example Usage
# Mock FastHTML app and routers for testingclass MockRouter:def__init__(self, prefix):self.prefix = prefixself.registered =Falsedef to_app(self, app):self.registered =True app.routers.append(self)class MockApp:def__init__(self):self.routers = []# Create mock app and routersapp = MockApp()main_router = MockRouter("/")settings_router = MockRouter("/settings")api_router = MockRouter("/api")# Register all at onceregister_routes(app, main_router, settings_router, api_router)# Verify all registeredprint(f"Number of routers registered: {len(app.routers)}")for router in app.routers:print(f" - {router.prefix}: registered={router.registered}")
Number of routers registered: 3
- /: registered=True
- /settings: registered=True
- /api: registered=True
Benefits
This simple helper provides:
Reduced boilerplate: One line instead of N lines for N routers
Clear intent: Explicitly shows “these are all the routes for this app”
Easier maintenance: Add/remove routers in one place
Less error-prone: Can’t accidentally forget to register a router
Flat-URL APIRouter (FastHTML 0.14 compatibility)
A thin APIRouter subclass that restores pre-0.14 flat URL behavior for routes defined inside factory functions. Consumers import this in place of fasthtml.common.APIRouter.
Background
FastHTML 0.14.0 introduced nested_name(f) (in fasthtml/core.py), which derives route URLs and rt_funcs attribute names from func.__qualname__ rather than func.__name__. For routes defined inside a factory function — the standard init_*_router(prefix) pattern across this ecosystem — __qualname__ includes the containing function’s name:
FastHTML 0.14 exposes no flag to disable this. Per design-system P9 (long-term solutions over quick fixes) and P11 (change-tolerant by design), we override APIRouter once here and consumers import from cjm_fasthtml_app_core.core.routing. When upstream FastHTML eventually exposes a flat_qualname=True (or equivalent) kwarg — likely, since the conflict will surface for other factory-pattern users — the revert is a single-file change behind the stable namespace.
APIRouter override that derives route URLs and rt_funcs names fromfunc.__name__ rather than FastHTML 0.14’s nested_name() (which prepends the containing function’s name to routes defined inside factory functions).
Restores pre-0.14 flat URL behavior for the init_*_router(prefix) factory pattern used across the cjm-* ecosystem.
Also overrides __getattr__ to raise a clean AttributeError when an attribute lookup falls through. FastHTML 0.14’s base implementation calls super().__getattr__(self, name), but object has no __getattr__, so that path produces a confusing 'super' object has no attribute '__getattr__' error instead of the expected AttributeError for any non-route attribute access.
Test
# Test cell: validate flat URLs across all five route patterns + clean __getattr__.# Mirrors the init_*_router factory pattern used across the cjm-* ecosystem# (the pattern that surfaced the FastHTML 0.14 incompatibility).def _test_apirouter_flat_urls():"""Factory-pattern routes produce flat URLs (pre-0.14 behavior).""" rt = APIRouter('/cs')@rtdef nav_up(): pass@rtdef update_viewport(): pass@rt('/custom_path')def some_handler(): pass@rt.getdef fetch_data(): pass@rt.post('/explicit_post')def submit_form(): pass@rtdef index(): pass# Bare @rt: URL from func.__name__, not __qualname__-derived nested nameassert nav_up.to() =='/cs/nav_up', f'got {nav_up.to()!r}'assert update_viewport.to() =='/cs/update_viewport', f'got {update_viewport.to()!r}'# Explicit path: preserved verbatimassert some_handler.to() =='/cs/custom_path', f'got {some_handler.to()!r}'# HTTP-method shortcut (bare): flat URL — confirms partialmethod re-bind worksassert fetch_data.to() =='/cs/fetch_data', f'got {fetch_data.to()!r}'# HTTP-method shortcut (explicit): preservedassert submit_form.to() =='/cs/explicit_post', f'got {submit_form.to()!r}'# index() special case: prefix + '/' (matches FastHTML's existing convention)assert index.to() =='/cs/', f'got {index.to()!r}'# rt_funcs accessibility uses flat func.__name__asserthasattr(rt.rt_funcs, 'nav_up')asserthasattr(rt.rt_funcs, 'some_handler')asserthasattr(rt.rt_funcs, 'fetch_data')asserthasattr(rt.rt_funcs, 'submit_form')# HTTP methods registered correctly through the re-bound partialmethods routes_by_func = {r[0].__name__: r for r in rt.routes}assert routes_by_func['fetch_data'][2] =='get'assert routes_by_func['submit_form'][2] =='post'return rtdef _test_apirouter_getattr_raises_clean_error():"""Non-route attribute access raises clean AttributeError, not the FastHTML 0.14 base-class `'super' object has no attribute '__getattr__'` error. """ rt = APIRouter('/cs')@rtdef known_route(): pass# Known route is accessible via rt.<name> (rt_funcs delegation)assert rt.known_route isnotNone# Unknown attribute raises clean AttributeError, not the upstream-FastHTML-0.14# 'super' object errortry: _ = rt.this_attribute_does_not_existexceptAttributeErroras e: msg =str(e)assert'this_attribute_does_not_exist'in msg, f"AttributeError msg: {msg!r}"assert'super'notin msg, (f"AttributeError leaked the FastHTML 0.14 'super' fallthrough error: {msg!r}" )else:raiseAssertionError("expected AttributeError on unknown attribute")_test_apirouter_flat_urls()_test_apirouter_getattr_raises_clean_error()print('✓ All flat-URL APIRouter assertions pass.')print('✓ Clean __getattr__ AttributeError verified.')