# FastAPI And Uvicorn Startup Use this reference when FastAPI owns the application and NiceGUI is one part of it. The central distinction is between **composing an ASGI application** and **starting an ASGI server**: - [`ui.run_with()`](https://github.com/zauberzeug/nicegui/blob/main/nicegui/ui_run_with.py) composes NiceGUI with a caller-owned FastAPI application. It does not start Uvicorn. - [`uvicorn.run()`](https://www.uvicorn.org/#running-programmatically) starts the server and tells it which ASGI application to serve. ## Ownership Model ```mermaid flowchart TD E["Project script: my-app"] --> M["main()"] M --> S["get_settings()"] M --> U["uvicorn.run()"] U --> F["create_app()"] F --> S F --> P["Parent FastAPI app"] P --> A["API routes and middleware"] P -->|"mount_path=/gui"| N["NiceGUI App"] U -->|"ASGI requests and lifespan"| P ``` The objects have separate responsibilities: | Object | Owner | Responsibility | | --- | --- | --- | | Parent `FastAPI` instance | Application code | Root ASGI app, API routes, middleware, lifespan, and mounted applications | | `Settings` instance | Application code | Immutable, process-local configuration snapshot shared by startup and composition | | `nicegui.app` | NiceGUI | A process-local [`App`](https://github.com/zauberzeug/nicegui/blob/main/nicegui/app/app.py) instance that subclasses `FastAPI` | | `ui.run_with(parent_app)` | NiceGUI integration | Configures NiceGUI, mounts `nicegui.app` into `parent_app`, and integrates lifecycle handling | | Uvicorn | Server process | Imports or receives the root ASGI app, opens sockets, drives lifespan, and serves requests | Uvicorn must serve the **parent FastAPI app** when using `ui.run_with()`. Passing `nicegui.app` to `ui.run_with()` is rejected because it would mount NiceGUI into itself and recurse on unmatched routes. ## Choose One Startup Mode ### Let NiceGUI Own Startup Use `ui.run()` when NiceGUI is the main application. Add ordinary FastAPI routes to the exported `nicegui.app` object: ```python from nicegui import app, ui @app.get('/healthz') def health() -> dict[str, str]: return {'status': 'ok'} @ui.page('/') def home() -> None: ui.label('Home') ui.run() ``` In this mode, NiceGUI configures and starts its own [Uvicorn-derived server](https://github.com/zauberzeug/nicegui/blob/main/nicegui/server.py). Do not also call `uvicorn.run()`. ### Let FastAPI Own The Application Use `ui.run_with()` when an existing FastAPI application owns middleware, API routers, OpenAPI configuration, lifespan resources, or deployment startup. The [official NiceGUI FastAPI example](https://github.com/zauberzeug/nicegui/blob/main/examples/fastapi/main.py) follows this model. `mount_path` controls where the NiceGUI application appears externally. A NiceGUI page declared as `/` is reachable at `/gui/` when mounted at `/gui`, while parent routes such as `/healthz` remain at the root. A dedicated UI prefix usually makes ownership and route conflicts clearer than mounting both applications at `/`. ## Canonical Factory Layout Keep application composition importable and server startup explicit: ```text . ├─ pyproject.toml └─ src/ └─ my_app/ ├─ __init__.py ├─ config.py └─ main.py ``` ```python title="src/my_app/config.py" from functools import cache from typing import Literal from pydantic import BaseModel, ConfigDict, Field, SecretStr from pydantic_settings import BaseSettings, SettingsConfigDict class ServerSettings(BaseModel): model_config = ConfigDict(frozen=True) host: str = '0.0.0.0' port: int = 8000 log_level: Literal['critical', 'error', 'warning', 'info', 'debug', 'trace'] = ( 'info' ) reload: bool = False class GuiSettings(BaseModel): model_config = ConfigDict(frozen=True) mount_path: str = '/gui' storage_secret: SecretStr | None = None class Settings(BaseSettings): model_config = SettingsConfigDict( env_prefix='MY_APP_', env_nested_delimiter='__', env_file='.env', env_file_encoding='utf-8', frozen=True, ) server: ServerSettings = Field(default_factory=ServerSettings) gui: GuiSettings = Field(default_factory=GuiSettings) @cache def get_settings() -> Settings: return Settings() ``` `ServerSettings` and `GuiSettings` inherit from `BaseModel` because they share one application owner, source policy, and process lifecycle. The root `BaseSettings` reads the sources once and validates one atomic snapshot. Environment variables use names such as `MY_APP_SERVER__PORT`, `MY_APP_SERVER__RELOAD`, `MY_APP_GUI__MOUNT_PATH`, and `MY_APP_GUI__STORAGE_SECRET`. The argument-free [`functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) provider is appropriate here because both the project entry point and Uvicorn's zero-argument factory need process-lifetime access. Each reload or worker process gets its own settings instance. Do not add override arguments to `get_settings()`; inject a `Settings` instance directly into `create_app()` in tests or alternate composition roots. See the [Pydantic settings implementation guide](../../pydantic-settings/SKILL.md) for source precedence, independent settings boundaries, cache clearing, and runtime reload guidance. ```python title="src/my_app/main.py" from collections.abc import AsyncGeneratorr from contextlib import asynccontextmanager import uvicorn from fastapi import FastAPI from nicegui import ui from my_app.config import Settings, get_settings @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None]: app.state.ready = True try: yield finally: app.state.ready = False def register_pages() -> None: @ui.page('/') def dashboard() -> None: ui.label('Dashboard') def create_app(settings: Settings | None = None) -> FastAPI: settings = settings or get_settings() app = FastAPI(lifespan=lifespan) app.state.settings = settings @app.get('/healthz') def health() -> dict[str, str]: return {'status': 'ok'} register_pages() ui.run_with( app, mount_path=settings.gui.mount_path, storage_secret=( settings.gui.storage_secret.get_secret_value() if settings.gui.storage_secret is not None else None ), ) return app def main() -> None: settings = get_settings() uvicorn.run( 'my_app.main:create_app', factory=True, host=settings.server.host, port=settings.server.port, log_level=settings.server.log_level, reload=settings.server.reload, ) if __name__ == '__main__': main() ``` The `storage_secret` is optional unless the application uses `ui.storage.user` or `ui.storage.browser`. `SecretStr` prevents accidental plaintext display in logs and model representations, while `get_secret_value()` unwraps it only at the NiceGUI integration boundary. Supply production secrets through environment variables or a supported settings secret source rather than committing them. The example passes an [import string and `factory=True`](https://www.uvicorn.org/settings/#application) to Uvicorn. Uvicorn imports `my_app.main`, calls the zero-argument `create_app` factory, and serves the returned parent FastAPI app. Import strings are also required when Uvicorn creates reload or worker subprocesses; passing `create_app()` directly only supports the simple single-process case. NiceGUI keeps framework state in its process-local app singleton. Treat `create_app()` as a once-per-worker factory. Calling it repeatedly in one interpreter can register the same pages and lifecycle handlers more than once; tests that create multiple apps must isolate or reset NiceGUI state. ## Lifespan Ordering The [ASGI lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) is driven by the server. Uvicorn sends startup before accepting requests and sends shutdown while terminating the process. Lifespan runs once per event loop, including once in each worker process. Current NiceGUI source integrates with the parent application by: 1. Capturing the parent FastAPI lifespan context. 2. Mounting NiceGUI's internal app on the parent. 3. Replacing the parent lifespan with a wrapper. 4. Starting NiceGUI before entering the original parent lifespan. 5. Exiting the original parent lifespan before shutting down NiceGUI. This exact ordering comes from the current [`ui.run_with` implementation](https://github.com/zauberzeug/nicegui/blob/main/nicegui/ui_run_with.py) and is version-sensitive. Check the pinned NiceGUI version before making one startup handler depend on another framework's internal ordering. Create database pools, HTTP clients, and similar resources in the parent [FastAPI lifespan](https://fastapi.tiangolo.com/advanced/events/), then close them after `yield`. Do not create event-loop-bound resources at import time or assume that globals are shared between workers. ## Expose The Server As A Project Script Map a command name to the no-argument startup function: ```toml title="pyproject.toml" [project] name = "my-app" version = "0.1.0" requires-python = ">=3.12" dependencies = [ "fastapi", "nicegui", "pydantic-settings", "uvicorn[standard]", ] [project.scripts] my-app = "my_app.main:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/my_app"] ``` Run the installed command through uv: ```bash uv run my-app ``` The uv [project entry-point documentation](https://docs.astral.sh/uv/concepts/projects/config/#entry-points) requires a build system so uv installs the project and generates its command. The `[project.scripts]` target follows the [PyPA entry-point specification](https://packaging.python.org/en/latest/specifications/entry-points/#use-for-scripts): its generated wrapper imports `main`, calls it without arguments, and uses the return value as the process exit status. Returning `None` means successful completion. The settings model now owns host, port, logging, reload, mount path, and storage-secret configuration. Add an explicit CLI settings source or another CLI parser only when the project command needs user-supplied arguments; the entry-point callable itself still receives no arguments. ## Development Reload Because `main()` supplies an import string, it can enable Uvicorn reload for local development: ```dotenv title=".env" MY_APP_SERVER__HOST=127.0.0.1 MY_APP_SERVER__RELOAD=true ``` The cached settings object is a process-start snapshot. Changing an environment variable or dotenv file does not mutate a running instance; restart the process, or let the development reloader create a new worker when a watched file changes. Keep reload disabled in production. Uvicorn documents [`reload` and `workers` as mutually exclusive](https://www.uvicorn.org/settings/#production), and each worker would have independent settings, NiceGUI state, lifespan resources, and WebSocket connections. Use one worker by default unless the application has explicitly validated session affinity and externalized every stateful dependency needed across processes. ## Anti-Patterns | Anti-pattern | Why it fails | Preferred approach | | --- | --- | --- | | `ui.run_with(nicegui.app)` | Mounts NiceGUI into itself | Pass a separately created `FastAPI()` instance | | Calling both `ui.run()` and `ui.run_with()` | Gives two paths responsibility for startup | Choose one ownership model | | `uvicorn.run(create_app(), reload=True)` | Reload subprocesses cannot import the app object | Use an import string with `factory=True` | | Calling `uvicorn.run()` at module import time | Importing the module starts a blocking server and breaks subprocess startup | Call it from `main()` | | Top-level `ui.label(...)` with `ui.run_with()` | Script-mode elements are discarded by this integration | Register UI in `@ui.page` functions or a root callable | | Multiple workers by default | Process-local UI state and WebSockets are not automatically shared | Start with one worker and validate a distributed design explicitly | | Reconstructing `Settings()` throughout the app | Re-reads sources and obscures the active configuration lifecycle | Inject the startup snapshot or use the argument-free provider at framework boundaries | | Adding kwargs to cached `get_settings()` | Retains one hidden process-lifetime instance per argument combination | Construct explicit `Settings(...)` overrides and inject them | ## Verification Use `TestClient` as a context manager so the parent ASGI lifespan runs: ```python from fastapi.testclient import TestClient from my_app.config import GuiSettings, Settings from my_app.main import create_app def test_application_routes() -> None: settings = Settings( gui=GuiSettings(storage_secret='test-storage-secret'), ) with TestClient(create_app(settings)) as client: assert client.get('/healthz').json() == {'status': 'ok'} assert client.get('/gui/').status_code == 200 ``` Also verify: - startup resources exist while the client context is active and are released afterward - the mounted UI returns HTML and parent API failures retain FastAPI's JSON responses - `uv run my-app` starts the server and responds on both the API and UI paths - shutdown signals complete without orphaned background tasks ## Primary Sources - [NiceGUI pages, routing, and FastAPI integration](https://www.nicegui.io/documentation/section_pages_routing) - [NiceGUI `ui.run_with` implementation](https://github.com/zauberzeug/nicegui/blob/main/nicegui/ui_run_with.py) - [NiceGUI FastAPI example](https://github.com/zauberzeug/nicegui/blob/main/examples/fastapi/main.py) - [FastAPI lifespan events](https://fastapi.tiangolo.com/advanced/events/) - [ASGI lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) - [Uvicorn settings](https://www.uvicorn.org/settings/) - [Uvicorn programmatic startup](https://www.uvicorn.org/#running-programmatically) - [Pydantic settings management](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/) - [`functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) - [uv project entry points](https://docs.astral.sh/uv/concepts/projects/config/#entry-points) - [PyPA entry points specification](https://packaging.python.org/en/latest/specifications/entry-points/)