Skip to content

FastAPI Integration

Asterios Raptis edited this page May 20, 2026 · 4 revisions

FastAPI Integration

PluginForge can mount plugin routes onto a FastAPI application. FastAPI is an optional dependency: install it separately when you need this integration.

pip install pluginforge fastapi

Note: the pluginforge[fastapi] extra existed pre-v0.6.0 but was non-functional (it referenced a dependency that was never declared as optional). It was removed in v0.6.0.

Mounting Routes

from fastapi import FastAPI
from pluginforge import PluginManager

app = FastAPI()
pm = PluginManager("config/app.yaml")
pm.discover_plugins()
pm.mount_routes(app)

Custom Prefix

The default prefix is /api. You can change it:

pm.mount_routes(app, prefix="/v2/api")

Idempotent since v0.8.0

mount_routes is idempotent: calling it twice with the same plugin set on the same FastAPI app is a no-op. Long pytest sweeps that re-enter FastAPI lifespans on a shared app no longer accumulate route registrations. Tracking state lives in a module-level WeakKeyDictionary[FastAPI, set[str]] inside pluginforge.fastapi_ext (per-app, weakref-keyed so the entry is released when the app is garbage-collected).

This fix closes a recursion-cascade Bibliogon reported (RecursionError in starlette.testclient.TestClient.wait_startup after long sweeps). pluginforge.testing.IsolatedPluginManager clears the tracking on context entry and exit; use it as the canonical test fixture for consumer-app suites that mount routes across many TestClient lifespans.

Plugin Routes

Plugins provide routes by overriding get_routes(). Plugins bring their own route structure via their routers:

from fastapi import APIRouter
from pluginforge import BasePlugin

class HelloPlugin(BasePlugin):
    name = "hello"

    def get_routes(self) -> list:
        router = APIRouter(prefix="/hello")

        @router.get("/greet")
        def greet():
            greeting = self.config.get("greeting", "Hello")
            return {"message": greeting}

        @router.get("/info")
        def info():
            return {"plugin": self.name, "version": self.version}

        return [router]

With default prefix, routes are available at:

GET /api/hello/greet  -> HelloPlugin.greet()
GET /api/hello/info   -> HelloPlugin.info()

Plugins control their own URL structure. An export plugin can define routes like /books/{book_id}/export/{format} and they will be mounted under the prefix:

GET /api/books/{book_id}/export/pdf

Single-Router Convention (v0.8.0)

get_routes() should return exactly one APIRouter. Nest namespace-separated sub-routers via router.include_router(sub_router).

# Recommended: one parent router with nested sub-routers.
panels_router = APIRouter(prefix="/panels")
panels_router.add_api_route("/", list_panels, methods=["GET"])

bubbles_router = APIRouter(prefix="/bubbles")
bubbles_router.add_api_route("/", list_bubbles, methods=["GET"])

class ComicsPlugin(BasePlugin):
    name = "comics"

    def get_routes(self) -> list:
        parent = APIRouter(prefix="/comics")
        parent.include_router(panels_router)
        parent.include_router(bubbles_router)
        return [parent]

Plugins returning more than one top-level router from get_routes() emit a DeprecationWarning since v0.8.0. The warning continues in v0.9.0; v0.10.0 may make it an error. See docs/guides/plugin-author.md for the full rationale and migration recipe.

Health Endpoint

You can expose a health endpoint using pm.health_check():

@app.get("/api/health")
def health():
    return pm.health_check()

Response:

{
    "export": {"status": "ok", "formats": ["epub", "pdf"]},
    "grammar": {"status": "error", "error": "LanguageTool API unreachable"}
}

Error Handling

  • If fastapi is not installed, mount_routes() raises ImportError with installation instructions
  • If the app argument is not a FastAPI instance, raises TypeError
  • Plugins that return empty routes from get_routes() are silently skipped

Clone this wiki locally