Class-Based NiceGUI Pages and Integrations: A Practical Guide¶
Objective: Master building structured NiceGUI applications with class-based pages, dependency injection, and clean architecture. When you need to build real applications—not prototypes—with auth, dashboards, API integrations, and maintainable code, this tutorial becomes your weapon of choice.
Most NiceGUI examples are single-file fever dreams: everything in main.py, global state drifting around like cosmic dust. That's fine for a quick prototype, but the moment you want a real app—auth, dashboards, API integrations—you need structure.
This tutorial shows how to build class-based NiceGUI pages with:
- A clean project layout
- Reusable page classes and components
- Centralized routing
- Integration points for external services (APIs, DBs, background workers)
Prerequisites¶
- Python >= 3.10
- NiceGUI >= 2.x
- Basic understanding of Python classes and async/await
1. Project Layout¶
Let's start with a small but scalable layout:
nicegui-app/
├── app.py # entrypoint
├── core/
│ ├── __init__.py
│ ├── app_factory.py # create_app() lives here
│ └── services.py # "business logic", API clients, db, etc
├── pages/
│ ├── __init__.py
│ ├── base.py # BasePage class
│ ├── home.py # HomePage
│ ├── dashboard.py # DashboardPage
│ └── form_demo.py # FormPage example
└── requirements.txt
requirements.txt¶
2. The Base Page Pattern¶
We want each page to be a class that:
- Knows its own route path
- Encapsulates UI construction (
build(),layout(), etc.) - Optionally injects shared services (API clients, repositories)
- Can be registered in a central router
2.1 pages/base.py¶
# pages/base.py
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional, Any
from nicegui import ui
class BasePage(ABC):
"""Abstract base for all pages.
Responsibilities:
- Define `route` (e.g., "/dashboard")
- Provide `register(app)` to hook into NiceGUI routing
- Implement `build()` to construct content
"""
# Route path, to be overridden
route: str = "/"
# Optional title; can be used to set browser title, header, etc.
title: str = "Untitled"
def __init__(self, services: Optional[dict[str, Any]] = None) -> None:
self.services = services or {}
@classmethod
def register(cls, app, services: Optional[dict[str, Any]] = None) -> None:
"""Attach this page to the NiceGUI app router."""
page_instance = cls(services=services)
@ui.page(cls.route)
def _() -> None:
page_instance.render()
def render(self) -> None:
"""Default render flow: layout + build."""
with ui.column().classes("w-full items-stretch gap-4 p-4"):
self._render_header()
self.build()
def _render_header(self) -> None:
with ui.row().classes("w-full justify-between items-center"):
ui.label(self.title).classes("text-2xl font-bold")
# Hook for page-level actions (buttons, filters, etc.)
self.render_actions()
def render_actions(self) -> None:
"""Override in subclasses for header actions."""
...
@abstractmethod
def build(self) -> None:
"""Subclasses must implement to build the page contents."""
raise NotImplementedError
This gives us a template:
route: where it livestitle: display / browser / headerrender(): calls_render_header()+build()register(app, services): binds it to@ui.page
3. Creating a Simple Home Page¶
3.1 pages/home.py¶
# pages/home.py
from __future__ import annotations
from nicegui import ui
from .base import BasePage
class HomePage(BasePage):
route = "/"
title = "Welcome to the NiceGUI App"
def render_actions(self) -> None:
# Example header actions
with ui.row().classes("gap-2"):
ui.button("Dashboard", on_click=lambda: ui.open("/dashboard"))
ui.button("Form Demo", on_click=lambda: ui.open("/form-demo"))
def build(self) -> None:
ui.label("This is the home page, built from a class.").classes("text-lg")
ui.markdown(
"""
- Navigate using the header buttons
- Each page is defined as a Python class
- Shared services are injected via the BasePage constructor
"""
)
with ui.row().classes("gap-4 mt-4"):
with ui.card():
ui.label("Quick Card")
ui.label("You can put any UI components in build().")
with ui.card():
ui.label("Another Card")
ui.button("Click me", on_click=lambda: ui.notify("Clicked on home page!"))
4. Injecting Services: API / DB / Whatever¶
We don't want each page to instantiate its own HTTP client or database connection. Instead, we define services in one place and pass them down.
4.1 core/services.py¶
# core/services.py
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import httpx
@dataclass
class ApiClient:
base_url: str
async def get_status(self) -> dict[str, Any]:
async with httpx.AsyncClient(base_url=self.base_url, timeout=5.0) as client:
r = await client.get("/status")
r.raise_for_status()
return r.json()
def create_services() -> dict[str, Any]:
"""Construct and return the service container."""
api_client = ApiClient(base_url="https://httpbin.org")
# Extend here with db clients, redis, etc.
return {
"api": api_client,
}
This gives us a simple "service container" dictionary.
5. A Dashboard Page Using Services¶
Now let's build a page that calls an API and renders a table / cards.
5.1 pages/dashboard.py¶
# pages/dashboard.py
from __future__ import annotations
from typing import Any
from nicegui import ui
from .base import BasePage
class DashboardPage(BasePage):
route = "/dashboard"
title = "Dashboard"
async def _refresh_status(self, label: ui.label) -> None:
api = self.services["api"]
label.text = "Loading..."
try:
data: dict[str, Any] = await api.get_status()
label.text = f"API status: {data.get('url', 'unknown')}"
ui.notify("Status refreshed", type="positive")
except Exception as e: # noqa: BLE001
label.text = "Error while fetching status"
ui.notify(f"Error: {e}", type="negative")
def build(self) -> None:
ui.label("Dashboard overview").classes("text-lg mb-2")
with ui.card().classes("w-full max-w-xl"):
status_label = ui.label("API status: unknown").classes("mb-2")
ui.button(
"Refresh status",
on_click=lambda: self._refresh_status(status_label),
)
with ui.row().classes("gap-4 mt-4"):
with ui.card():
ui.label("Metric A")
ui.label("42")
with ui.card():
ui.label("Metric B")
ui.label("1337")
Note: - We access self.services["api"] - We use an async helper _refresh_status and hook it with on_click - NiceGUI is fine with async callables
6. A Form Page with Validation¶
Let's add another class-based page that showcases forms, validation, and interactions.
6.1 pages/form_demo.py¶
# pages/form_demo.py
from __future__ import annotations
from nicegui import ui
from .base import BasePage
class FormDemoPage(BasePage):
route = "/form-demo"
title = "Form Demo"
def build(self) -> None:
ui.label("User Input Form").classes("text-lg mb-2")
with ui.card().classes("w-full max-w-lg"):
name = ui.input("Name").props("outlined").classes("mb-2")
email = ui.input("Email").props("outlined").classes("mb-2")
agree = ui.checkbox("I agree to the terms").classes("mb-2")
result = ui.label("").classes("mt-2 text-sm text-gray-500")
def submit() -> None:
errors = []
if not name.value:
errors.append("Name is required.")
if not email.value or "@" not in email.value:
errors.append("Valid email is required.")
if not agree.value:
errors.append("You must agree to the terms.")
if errors:
ui.notify("\n".join(errors), type="negative")
result.text = "Errors: " + "; ".join(errors)
else:
ui.notify("Form submitted!", type="positive")
result.text = f"Submitted: {name.value} <{email.value}>"
ui.button("Submit", on_click=submit)
7. Wiring It All Together: App Factory¶
Now we need a single app factory that:
- Builds the NiceGUI app
- Creates the shared services
- Registers all page classes
7.1 core/app_factory.py¶
# core/app_factory.py
from __future__ import annotations
from nicegui import ui
from typing import Type
from pages.base import BasePage
from pages.home import HomePage
from pages.dashboard import DashboardPage
from pages.form_demo import FormDemoPage
from core.services import create_services
def create_app():
"""Create and configure the NiceGUI app with class-based pages."""
services = create_services()
# List of page classes to register
page_classes: list[Type[BasePage]] = [
HomePage,
DashboardPage,
FormDemoPage,
]
# Register each page
for page_cls in page_classes:
page_cls.register(ui, services=services)
# Optional: global header/footer, default layouts, etc.
# For example, a simple top-level menu on every page via NiceGUI's `ui.header`:
@ui.header()
def header():
with ui.row().classes("w-full justify-between items-center px-4 py-2"):
ui.label("NiceGUI Class-Based App").classes("text-lg font-semibold")
with ui.row().classes("gap-2"):
ui.button("Home", on_click=lambda: ui.open("/"))
ui.button("Dashboard", on_click=lambda: ui.open("/dashboard"))
ui.button("Form", on_click=lambda: ui.open("/form-demo"))
return ui
8. Entry Point¶
8.1 app.py¶
# app.py
from __future__ import annotations
from core.app_factory import create_app
app = create_app()
if __name__ in {"__main__", "__mp_main__"}:
# Default NiceGUI run:
app.run(
title="NiceGUI Class-Based Pages",
reload=False, # set True in dev if desired
host="0.0.0.0",
port=8080,
)
Run it:
# Option 1: Direct Python
python app.py
# Option 2: Uvicorn (if using FastAPI integration)
uvicorn app:app --reload
9. Integration Patterns¶
Now that the skeleton is alive, here are some patterns for more serious integration.
9.1: FastAPI + NiceGUI in One Process¶
NiceGUI exposes a FastAPI app via ui.run_with / app.fastapi. A common pattern:
- Use FastAPI for REST/webhook endpoints
- Use NiceGUI for UI
- Share service container between them
Skeleton:
# app.py
from nicegui import ui
from fastapi import FastAPI
from core.app_factory import create_app
from core.services import create_services
services = create_services()
ng_app = create_app() # registers pages, etc.
fastapi_app = FastAPI()
# Example API endpoint
@fastapi_app.get("/api/ping")
async def ping():
api = services["api"]
data = await api.get_status()
return {"status": "ok", "upstream": data}
ui.run_with(
fastapi_app,
title="NiceGUI + FastAPI",
host="0.0.0.0",
port=8080,
)
Now:
http://localhost:8080/→ NiceGUI UIhttp://localhost:8080/api/ping→ FastAPI JSON
Pages keep using the same service container.
9.2: Page-Level Dependencies¶
Sometimes you don't want to pass the entire service container; you want typed attributes.
You can wrap the base to provide typed accessors:
# pages/base.py (alt pattern)
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.services import ApiClient
@dataclass
class TypedServices:
api: "ApiClient"
class BasePage(ABC):
# ...
def __init__(self, services: dict[str, Any]) -> None:
self.services_raw = services
self.s = TypedServices(api=services["api"])
Then in page:
# pages/dashboard.py
class DashboardPage(BasePage):
# ...
async def _refresh_status(self, label: ui.label) -> None:
data = await self.s.api.get_status()
# ...
9.3: Background Tasks & Schedulers¶
For periodic tasks (polling metrics, refreshing dashboards), you can:
- Use
asyncio.create_taskfrom within a page - Or integrate external schedulers (APScheduler, Prefect, etc.) and push updates via NiceGUI
Example: simple background update:
# in a Page.build()
import asyncio
counter_label = ui.label("Counter: 0")
self.counter = 0 # attach to instance
async def updater():
while True:
await asyncio.sleep(1)
self.counter += 1
counter_label.text = f"Counter: {self.counter}"
asyncio.create_task(updater())
Just be aware you're now dancing with concurrency and lifetime—good for small toys, but for production you'll want a more disciplined approach.
10. Integration with Prefect¶
Let's build a real-world example: a NiceGUI dashboard that displays Prefect flow runs and allows triggering new runs.
10.1 core/services.py (Extended)¶
# core/services.py (extended)
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import httpx
@dataclass
class ApiClient:
base_url: str
async def get_status(self) -> dict[str, Any]:
async with httpx.AsyncClient(base_url=self.base_url, timeout=5.0) as client:
r = await client.get("/status")
r.raise_for_status()
return r.json()
@dataclass
class PrefectClient:
api_url: str
api_key: str | None = None
async def get_flow_runs(
self, limit: int = 10, flow_id: str | None = None
) -> list[dict[str, Any]]:
"""Fetch recent flow runs from Prefect API."""
headers = {}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
params = {"limit": limit}
if flow_id:
params["flow_id"] = flow_id
async with httpx.AsyncClient(
base_url=self.api_url, headers=headers, timeout=10.0
) as client:
r = await client.get("/flow_runs", params=params)
r.raise_for_status()
return r.json()
async def trigger_flow_run(self, deployment_id: str) -> dict[str, Any]:
"""Trigger a new flow run."""
headers = {}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
async with httpx.AsyncClient(
base_url=self.api_url, headers=headers, timeout=10.0
) as client:
r = await client.post(
f"/deployments/{deployment_id}/create_flow_run",
json={},
)
r.raise_for_status()
return r.json()
def create_services() -> dict[str, Any]:
"""Construct and return the service container."""
import os
api_client = ApiClient(base_url="https://httpbin.org")
prefect_client = PrefectClient(
api_url=os.getenv("PREFECT_API_URL", "http://localhost:4200/api"),
api_key=os.getenv("PREFECT_API_KEY"),
)
return {
"api": api_client,
"prefect": prefect_client,
}
10.2 pages/prefect_dashboard.py¶
# pages/prefect_dashboard.py
from __future__ import annotations
from typing import Any
from nicegui import ui
from .base import BasePage
class PrefectDashboardPage(BasePage):
route = "/prefect"
title = "Prefect Dashboard"
def __init__(self, services: dict[str, Any] | None = None) -> None:
super().__init__(services)
self.flow_runs: list[dict[str, Any]] = []
self.runs_table: ui.table | None = None
async def _refresh_runs(self) -> None:
"""Fetch and display Prefect flow runs."""
prefect = self.services["prefect"]
try:
self.flow_runs = await prefect.get_flow_runs(limit=20)
if self.runs_table:
self.runs_table.rows = [
{
"id": run.get("id", "unknown"),
"name": run.get("name", "unnamed"),
"state": run.get("state", {}).get("type", "unknown"),
"start_time": run.get("start_time", "N/A"),
}
for run in self.flow_runs
]
ui.notify(f"Loaded {len(self.flow_runs)} flow runs", type="positive")
except Exception as e: # noqa: BLE001
ui.notify(f"Error loading runs: {e}", type="negative")
async def _trigger_run(self, deployment_id: str) -> None:
"""Trigger a new flow run."""
prefect = self.services["prefect"]
try:
result = await prefect.trigger_flow_run(deployment_id)
ui.notify(f"Flow run created: {result.get('id')}", type="positive")
await self._refresh_runs()
except Exception as e: # noqa: BLE001
ui.notify(f"Error triggering run: {e}", type="negative")
def build(self) -> None:
ui.label("Prefect Flow Runs").classes("text-lg mb-2")
with ui.row().classes("gap-2 mb-4"):
ui.button("Refresh", on_click=self._refresh_runs)
ui.button("Auto-refresh (10s)", on_click=self._start_auto_refresh)
columns = [
{"name": "id", "label": "Run ID", "field": "id"},
{"name": "name", "label": "Name", "field": "name"},
{"name": "state", "label": "State", "field": "state"},
{"name": "start_time", "label": "Start Time", "field": "start_time"},
]
self.runs_table = ui.table(
columns=columns,
rows=[],
row_key="id",
).classes("w-full")
# Initial load
self._refresh_runs()
def _start_auto_refresh(self) -> None:
"""Start auto-refreshing the table every 10 seconds."""
import asyncio
async def refresh_loop():
while True:
await asyncio.sleep(10)
await self._refresh_runs()
asyncio.create_task(refresh_loop())
ui.notify("Auto-refresh started", type="info")
10.3 Update app_factory.py¶
# core/app_factory.py (add PrefectDashboardPage)
from pages.prefect_dashboard import PrefectDashboardPage
# In create_app():
page_classes: list[Type[BasePage]] = [
HomePage,
DashboardPage,
FormDemoPage,
PrefectDashboardPage, # Add this
]
11. Integration with PostgreSQL¶
Let's add a page that queries PostgreSQL and displays data in real-time.
11.1 core/services.py (Add Postgres)¶
# core/services.py (add Postgres client)
import os
from typing import Any
import asyncpg
@dataclass
class PostgresClient:
connection_string: str
pool: asyncpg.Pool | None = None
async def get_pool(self) -> asyncpg.Pool:
"""Get or create connection pool."""
if self.pool is None:
self.pool = await asyncpg.create_pool(self.connection_string)
return self.pool
async def query(self, sql: str, *args: Any) -> list[dict[str, Any]]:
"""Execute query and return rows as dicts."""
pool = await self.get_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(sql, *args)
return [dict(row) for row in rows]
async def execute(self, sql: str, *args: Any) -> str:
"""Execute statement and return result."""
pool = await self.get_pool()
async with pool.acquire() as conn:
return await conn.execute(sql, *args)
def create_services() -> dict[str, Any]:
"""Construct and return the service container."""
api_client = ApiClient(base_url="https://httpbin.org")
prefect_client = PrefectClient(
api_url=os.getenv("PREFECT_API_URL", "http://localhost:4200/api"),
api_key=os.getenv("PREFECT_API_KEY"),
)
postgres_client = PostgresClient(
connection_string=os.getenv(
"DATABASE_URL", "postgresql://user:pass@localhost/dbname"
)
)
return {
"api": api_client,
"prefect": prefect_client,
"postgres": postgres_client,
}
11.2 pages/postgres_dashboard.py¶
# pages/postgres_dashboard.py
from __future__ import annotations
from typing import Any
from nicegui import ui
from .base import BasePage
class PostgresDashboardPage(BasePage):
route = "/postgres"
title = "PostgreSQL Dashboard"
def __init__(self, services: dict[str, Any] | None = None) -> None:
super().__init__(services)
self.query_result: list[dict[str, Any]] = []
self.result_table: ui.table | None = None
async def _execute_query(self, sql_input: ui.textarea) -> None:
"""Execute SQL query and display results."""
postgres = self.services["postgres"]
sql = sql_input.value.strip()
if not sql:
ui.notify("Please enter a SQL query", type="warning")
return
try:
self.query_result = await postgres.query(sql)
if self.result_table:
columns = (
[{"name": k, "label": k, "field": k} for k in self.query_result[0].keys()]
if self.query_result
else []
)
self.result_table.columns = columns
self.result_table.rows = self.query_result
ui.notify(f"Query returned {len(self.query_result)} rows", type="positive")
except Exception as e: # noqa: BLE001
ui.notify(f"Query error: {e}", type="negative")
def build(self) -> None:
ui.label("PostgreSQL Query Interface").classes("text-lg mb-2")
with ui.card().classes("w-full"):
sql_input = ui.textarea(
label="SQL Query",
placeholder="SELECT * FROM table LIMIT 10;",
).classes("w-full mb-2")
sql_input.value = "SELECT version();"
ui.button(
"Execute",
on_click=lambda: self._execute_query(sql_input),
).classes("mb-4")
columns = [{"name": "result", "label": "Result", "field": "result"}]
self.result_table = ui.table(
columns=columns,
rows=[],
).classes("w-full")
11.3 Update requirements.txt¶
12. Integration with Redis¶
For caching and real-time updates, let's add Redis support.
12.1 core/services.py (Add Redis)¶
# core/services.py (add Redis)
import redis.asyncio as redis
@dataclass
class RedisClient:
url: str
client: redis.Redis | None = None
async def get_client(self) -> redis.Redis:
"""Get or create Redis client."""
if self.client is None:
self.client = redis.from_url(self.url, decode_responses=True)
return self.client
async def get(self, key: str) -> str | None:
"""Get value from Redis."""
client = await self.get_client()
return await client.get(key)
async def set(self, key: str, value: str, ex: int | None = None) -> bool:
"""Set value in Redis."""
client = await self.get_client()
return await client.set(key, value, ex=ex)
async def keys(self, pattern: str = "*") -> list[str]:
"""Get keys matching pattern."""
client = await self.get_client()
return await client.keys(pattern)
def create_services() -> dict[str, Any]:
"""Construct and return the service container."""
import os
# ... existing services ...
redis_client = RedisClient(url=os.getenv("REDIS_URL", "redis://localhost:6379"))
return {
"api": api_client,
"prefect": prefect_client,
"postgres": postgres_client,
"redis": redis_client,
}
12.2 pages/redis_dashboard.py¶
# pages/redis_dashboard.py
from __future__ import annotations
from typing import Any
from nicegui import ui
from .base import BasePage
class RedisDashboardPage(BasePage):
route = "/redis"
title = "Redis Dashboard"
async def _get_value(self, key_input: ui.input, value_label: ui.label) -> None:
"""Get value from Redis."""
redis_client = self.services["redis"]
key = key_input.value
if not key:
ui.notify("Please enter a key", type="warning")
return
try:
value = await redis_client.get(key)
value_label.text = f"Value: {value if value else '(nil)'}"
ui.notify("Value retrieved", type="positive")
except Exception as e: # noqa: BLE001
ui.notify(f"Error: {e}", type="negative")
async def _set_value(
self, key_input: ui.input, value_input: ui.input
) -> None:
"""Set value in Redis."""
redis_client = self.services["redis"]
key = key_input.value
value = value_input.value
if not key:
ui.notify("Please enter a key", type="warning")
return
try:
await redis_client.set(key, value)
ui.notify(f"Set {key} = {value}", type="positive")
except Exception as e: # noqa: BLE001
ui.notify(f"Error: {e}", type="negative")
async def _list_keys(self, keys_list: ui.column) -> None:
"""List all keys in Redis."""
redis_client = self.services["redis"]
try:
keys = await redis_client.keys()
keys_list.clear()
with keys_list:
for key in keys[:100]: # Limit to 100 keys
ui.label(key).classes("text-sm")
ui.notify(f"Found {len(keys)} keys", type="positive")
except Exception as e: # noqa: BLE001
ui.notify(f"Error: {e}", type="negative")
def build(self) -> None:
ui.label("Redis Operations").classes("text-lg mb-2")
with ui.card().classes("w-full max-w-2xl"):
key_input = ui.input("Key").classes("mb-2")
value_input = ui.input("Value").classes("mb-2")
value_label = ui.label("Value: (not loaded)").classes("mb-2")
with ui.row().classes("gap-2 mb-4"):
ui.button(
"Get",
on_click=lambda: self._get_value(key_input, value_label),
)
ui.button(
"Set",
on_click=lambda: self._set_value(key_input, value_input),
)
ui.separator()
ui.label("Keys").classes("text-md font-semibold mb-2")
keys_list = ui.column().classes("w-full max-h-64 overflow-auto")
ui.button(
"List Keys",
on_click=lambda: self._list_keys(keys_list),
)
12.3 Update requirements.txt¶
13. Data Flow Diagram¶
flowchart LR
subgraph Services["Services / Backend"]
API["ApiClient (httpx)"]
Prefect["PrefectClient"]
Postgres["PostgresClient (asyncpg)"]
Redis["RedisClient"]
end
subgraph UI["NiceGUI App"]
Home["HomePage"]
Dash["DashboardPage"]
Form["FormDemoPage"]
PrefDash["PrefectDashboardPage"]
PgDash["PostgresDashboardPage"]
RedisDash["RedisDashboardPage"]
end
AppFactory["create_app()"] --> UI
AppFactory --> Services
Home --> Services
Dash --> API
PrefDash --> Prefect
PgDash --> Postgres
RedisDash --> Redis
Prefect -->|"HTTP / JSON"| PrefAPI["Prefect API"]
Postgres -->|"SQL"| PgDB["PostgreSQL"]
Redis -->|"Commands"| RedisServer["Redis Server"] The core idea:
- App factory builds services + registers pages
- Pages take services as dependencies
- Everything stays in tidy modules instead of a global ball of state
14. Complete Example: All Together¶
Here's a complete app.py that ties everything together:
# app.py
from __future__ import annotations
import os
from nicegui import ui
from fastapi import FastAPI
from core.app_factory import create_app
from core.services import create_services
# Create services
services = create_services()
# Create NiceGUI app with all pages
ng_app = create_app()
# Create FastAPI app for API endpoints
fastapi_app = FastAPI()
@fastapi_app.get("/api/health")
async def health():
"""Health check endpoint."""
return {"status": "ok", "services": list(services.keys())}
@fastapi_app.get("/api/services")
async def list_services():
"""List available services."""
return {"services": list(services.keys())}
# Run NiceGUI with FastAPI
if __name__ in {"__main__", "__mp_main__"}:
ui.run_with(
fastapi_app,
title="NiceGUI Class-Based App",
reload=os.getenv("NICEGUI_RELOAD", "false").lower() == "true",
host="0.0.0.0",
port=int(os.getenv("PORT", "8080")),
)
15. Checklist for Extending This¶
If you're going to hand this to an agent and tell it to mutate reality:
- ✅ Add more page classes under
pages/for specific domains (metrics, admin, logs) - ✅ Implement a shared
BaseLayoutwith sidebar navigation and theming - ✅ Integrate authentication (e.g., JWT, OIDC) in an
AuthPage+ guards around routes - ✅ Add a
SettingsPagethat persists config to a DB or file - ✅ Add tests for service layer and simple smoke tests for NiceGUI pages (headless)
- ✅ Add error boundaries and global error handling
- ✅ Implement real-time updates via WebSockets or Server-Sent Events
- ✅ Add logging and monitoring integration
- ✅ Create reusable UI components (cards, tables, forms) in a
components/directory
16. Production Considerations¶
16.1 Error Handling¶
Add global error handling:
# core/app_factory.py
@ui.exception_handler
def handle_exception(e: Exception):
ui.notify(f"Error: {e}", type="negative")
# Log to your logging service
16.2 Authentication Guards¶
Add route protection:
# pages/base.py (extended)
class BasePage(ABC):
# ...
requires_auth: bool = False
def render(self) -> None:
if self.requires_auth and not self._is_authenticated():
ui.open("/login")
return
# ... rest of render
16.3 Environment Configuration¶
Use environment variables for all configuration:
# core/config.py
import os
from dataclasses import dataclass
@dataclass
class Config:
prefect_api_url: str = os.getenv("PREFECT_API_URL", "http://localhost:4200/api")
prefect_api_key: str | None = os.getenv("PREFECT_API_KEY")
database_url: str = os.getenv("DATABASE_URL", "postgresql://localhost/db")
redis_url: str = os.getenv("REDIS_URL", "redis://localhost:6379")
See Also¶
- FastAPI Geospatial - High-performance spatial APIs with FastAPI
- API Development - Building high-performance, geospatial-aware APIs
- Python Async Best Practices - Write coroutines that don't betray you
- Prefect FIFO Flow with Redis - Prefect workflow patterns
This tutorial provides the complete machinery for building structured NiceGUI applications. The patterns scale from prototypes to production, from simple pages to complex dashboards with multiple backend integrations.