跪拜 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 ↗