NiceGUI Application Architecture¶
Load this reference for application composition, package boundaries, and optional subsystem decisions.
Baseline Package Boundaries¶
main.py: process entry point and app factory exposure.bootstrap.py: app composition, router wiring, page registration, and lifespan orchestration.config.py: typed settings and environment parsing.logging.py: centralized logging setup.api/: HTTP transport that delegates to services.services/: business and use-case logic.ui/pages/: route-level NiceGUI pages.ui/components/: shared presentation building blocks.
Recommended base shape:
.
├─ pyproject.toml
├─ .env.example
├─ src/
│ └─ app/
│ ├─ __init__.py
│ ├─ main.py
│ ├─ config.py
│ ├─ logging.py
│ ├─ api/
│ │ ├─ __init__.py
│ │ └─ health.py
│ ├─ services/
│ │ ├─ __init__.py
│ │ └─ example_service.py
│ └─ ui/
│ ├─ __init__.py
│ ├─ components/
│ │ ├─ __init__.py
│ │ └─ nav.py
│ └─ pages/
│ ├─ __init__.py
│ ├─ home.py
│ ├─ dashboard.py
│ └─ about.py
└─ tests/
├─ test_health.py
└─ test_pages_registration.py
Required Baseline Behavior¶
- FastAPI is the base ASGI app.
create_app()composes routes, resources, and NiceGUI.- Lifespan owns startup and shutdown resources.
- NiceGUI pages are modular and explicitly registered.
- FastAPI exposes a health route such as
/healthz. - Imports do not trigger runtime global side effects.
For the ownership relationship between a caller-created FastAPI app, nicegui.app, ui.run_with(), Uvicorn, and a packaged startup command, load FastAPI and Uvicorn startup.
Dependency Direction¶
Prefer:
main/bootstrap->config/logging+api+ui/pages+servicesapi->servicesui/pages->ui/components+servicesservices-> helpers, clients, anddb/when enabled
Avoid imports from services back into API or UI modules.
Page And Component Ownership¶
Page modules should be a thin route-level composition layer. A page resolves route inputs and page-scoped dependencies, establishes the page shell, composes reusable components, and wires only the interactions that cross component boundaries. It should not contain a component's internal element tree, field bindings, refresh logic, domain rules, persistence, or long-running synchronous work.
Extract a presentation pattern to ui/components/ when it appears on two or more pages or owns a meaningful state or interaction boundary. Reusable components should accept initial data, use-case functions, and event callbacks explicitly instead of importing page state or business services implicitly.
For page composition, responsive layout, Quasar props, and CSS customization, load styling and customization.
Reusable Component Contract¶
In this architecture, a "component" is an application-level composition pattern, not necessarily a custom Vue component or a subclass of NiceGUI Element. Its usual shape is:
- A typed dataclass represents the component's public handle and local UI state.
- A render or factory function creates one component instance, builds its element subtree, and binds elements to that instance.
- The function returns the instance so its caller can read or change intentional state, invoke public actions, or coordinate it with another component.
- Internal elements, event handlers, validation feedback, and refreshable regions remain private to the component unless an imperative element handle is intentionally part of its API.
Use binding.bindable_dataclass for fields that drive or receive element properties. Plain @dataclass is sufficient when the returned object only groups element handles or callbacks and does not need immediate field propagation. Use bindable_fields when the dataclass also stores injected dependencies or other fields that should not participate in NiceGUI's binding graph.
from collections.abc import Awaitable, Callable
from dataclasses import field
from nicegui import binding, ui
Search = Callable[[str], Awaitable[list[str]]]
@binding.bindable_dataclass(bindable_fields={"query", "busy", "items"})
class SearchPanel:
search: Search = field(repr=False)
query: str = ""
busy: bool = False
items: list[str] = field(default_factory=list)
@ui.refreshable_method
def render_results(self) -> None:
if not self.items:
ui.label("No results")
for item in self.items:
ui.label(item)
async def submit(self) -> None:
if self.busy:
return
self.busy = True
try:
self.items = await self.search(self.query)
await self.render_results.refresh()
finally:
self.busy = False
def render_search_panel(search: Search) -> SearchPanel:
panel = SearchPanel(search=search)
with ui.column().classes("w-full gap-3"):
ui.input("Search").bind_value(panel, "query")
ui.button("Search", on_click=panel.submit).bind_enabled_from(
panel,
"busy",
backward=lambda busy: not busy,
)
ui.label().bind_text_from(
panel,
"items",
backward=lambda items: f"{len(items)} results",
)
panel.render_results()
return panel
The returned dataclass is the component API. Its bindable fields synchronize stable element properties, while render_results() owns a bounded region whose child structure changes with items. The injected search callable preserves dependency direction: the component can invoke a use case without locating a service globally.
@ui.refreshable_method is the instance-oriented refresh surface for this pattern. NiceGUI records refresh targets by method instance, allowing each page-created component object to refresh independently. Detailed target, argument, async, and lifecycle behavior is documented under refreshable component regions.
Thin Page Example¶
from nicegui import ui
from app.services.catalog import search_catalog
from app.ui.components.search_panel import render_search_panel
@ui.page("/catalog")
def catalog_page() -> None:
with ui.column().classes("mx-auto w-full max-w-5xl gap-6"):
ui.label("Catalog").classes("text-2xl font-semibold")
render_search_panel(search_catalog)
The page owns the route and composition. The component owns its controls, binding graph, feedback state, and structural refresh. The service owns search rules and data access. If two component handles must coordinate, keep the page wiring declarative, such as subscribing one component's public event to another component's public refresh action; move orchestration with domain meaning into a service.
Component Lifetime¶
Create component state during each page build unless sharing is deliberate. A module-global component dataclass can leak UI state across clients, and a module-global @ui.refreshable function can refresh every recorded target. Do not retain returned handles beyond their owning client without an explicit cleanup and stale-client policy.
Bindings to elements are removed with NiceGUI's element lifecycle. Refreshing a region deletes and recreates the elements inside that region, so external code should retain the component handle rather than private child element references. Component-owned subscriptions, timers, and background tasks must follow the client deletion rules in interaction mechanics.
Optional Persistence¶
Use only when the product requires durable data.
- Create one engine and sessionmaker per process.
- Provide request- or operation-scoped sessions with
yield. - Keep transaction boundaries explicit in service or repository flows.
- Never share sessions across concurrent tasks.
- Use Alembic as the schema migration source of truth.
Optional LangGraph AI¶
Use only for multi-step orchestration, resumable work, streaming, or human approval.
- Keep graph internals outside API and UI modules.
- Invoke graphs through a service such as
services/ai_service.py. - Use stable thread or session IDs for resumable flows.
- Keep interrupt payloads JSON-serializable.
Optional Mounted Docs¶
Use only when generated docs must be served by the application.
Suggested settings:
docs_enableddocs_mount_pathdocs_site_dirdocs_require_build
Mount docs in the composition layer, normalize the mount path, avoid route conflicts, and define behavior for missing build artifacts.
Async And Responsiveness¶
- Use
async defwhere a handler or service path performs I/O. - Prefer non-blocking clients and libraries.
- Offload CPU-heavy work to worker or background execution.
- Define progress, cancellation, timeout, completion, and error states for long actions.
- Stream or chunk results when workflows are long-running or multi-step.
Testing Minimums¶
- Test the FastAPI health route.
- Test page registration wiring.
- If persistence is enabled, test session lifecycle and rollback behavior.
- If AI is enabled, test happy paths and interrupt/resume behavior.
- If docs are enabled, test the mounted index route.
- For long actions, test loading, completion, and error states.