跪拜 Guibai
← All articles
Frontend · Backend · FastAPI

A Full-Stack Admin Panel’s Startup Sequence, from CLI to Dynamic Route Injection

By 创新技术阁 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

A startup sequence that gets environment loading, plugin discovery, and route injection order wrong produces silent misconfigurations — wrong DB credentials, missing routes, or cached stale settings. This walkthrough surfaces the exact ordering constraints and the factory-plus-lifespan pattern that prevents those failures in production.

Summary

The backend boot sequence starts with a Typer CLI command that sets the environment and clears a pydantic-settings cache to force re-reading the correct .env file. Uvicorn then launches a factory-built FastAPI app, which assembles middleware, registers built-in routers, and runs a DynamicRouter that scans `backend/app/plugin/module_*/**/controller.py` files, mapping each `module_xxx` directory to a `/xxx` route prefix with deduplication and failure isolation. The lifespan context manager seeds the database, connects Redis, warms caches, and starts APScheduler before printing a readiness panel.

On the frontend, Vite loads environment variables and auto-imports Vue APIs and Element Plus components. The entry script enforces a cascading style order: Element Plus base, Tailwind utilities, then project SCSS overrides. Plugin initialization follows a hard dependency chain — Pinia store first, then Vue Router with hash-mode history, permission directives, global error handlers, and finally editor/i18n modules. Route guards intercept navigation: unauthenticated users are redirected to login, while authenticated users trigger an API call to fetch the backend menu tree, which `MenuProcessor` converts into Vue routes and injects via `router.addRoute` before re-triggering navigation.

Takeaways
— Running `uv run main.py run --env=dev` sets the ENVIRONMENT variable and calls `get_settings.cache_clear()` so pydantic-settings re-reads the correct `.env.dev` file instead of falling back to defaults.
— Uvicorn uses `factory=True` with `"main:create_app"`, so every worker restart gets a clean FastAPI instance rather than a mutated singleton.
— Middleware is assembled in reverse order inside `create_app`, and built-in routers mount before the DynamicRouter scans plugin directories.
— Plugin directories must be named `module_*` and contain a `controller.py` that exposes an `APIRouter`; the system maps `module_pay` to the `/pay` prefix automatically.
— The DynamicRouter caches its scan result, deduplicates routers by Python object id, and prints diagnostic traces on import failures without crashing the whole server.
— The lifespan startup phase seeds the database, connects Redis, warms parameter and dictionary caches, and initializes APScheduler from persisted job definitions.
— Frontend style loading is ordered: Element Plus base CSS, then Tailwind, then project SCSS — ensuring overrides apply predictably.
— Plugin initialization runs Pinia first, then Router, then permission directives and error handlers; swapping this order breaks route guards that depend on user state.
— The Vue Router uses hash-mode history and only registers `/login`, `404`, `403`, and `/redirect` statically; all other routes are fetched from the backend menu API and injected dynamically after authentication.
— After dynamic route injection, the guard calls `next({ ...to, replace: true })` to force a re-navigation so the new routes actually match.
Conclusions

The original code had a timing bug where environment variables were set after settings were already loaded, causing the system to silently use defaults — a class of error that’s easy to miss in integration tests but breaks per-environment configs.

Using a factory pattern with Uvicorn’s `factory=True` is underdocumented in the FastAPI ecosystem; this project shows it working in practice with hot-reload and multi-process deployments.

The DynamicRouter’s decision to isolate failures per plugin — logging the trace and continuing — is a deliberate robustness trade-off: a broken plugin won’t take down the entire admin panel, which matters in multi-tenant or plugin-market scenarios.

Frontend route injection from a server-side permission tree means the SPA has no compiled-in route map for protected pages; the entire navigation structure is a runtime artifact, which complicates code-splitting and static analysis but centralizes access control.

Concepts & terms
DynamicRouterRegistry
A singleton in `app/core/discover.py` that scans `backend/app/plugin/module_*/**/controller.py` at startup, builds a cached routing tree, and mounts each plugin under a prefix derived from its directory name — with id-based deduplication and per-plugin failure isolation.
Lifespan async context manager
A FastAPI pattern (the `lifespan` parameter) that yields control during startup and shutdown, allowing async resource initialization — database seeding, Redis connection, cache warming, scheduler start — before the server accepts requests, and orderly teardown on exit.
MenuProcessor
A frontend utility that transforms the backend’s hierarchical permission menu tree into Vue Router route records, enabling fully dynamic client-side routing based on the logged-in user’s permissions.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗