Skip to content

FastAPI

A realistic use of pluginkit: make a web app extensible, so features are added by installing a plugin rather than editing the app. The host owns one hook, register_routes, and each plugin attaches its own endpoints to a shared router.

Full runnable example: examples/cookbook/fastapi_app.py.

The hook

from fastapi import APIRouter, FastAPI
from pluginkit import Extension, ExtensionPoint, PluginManager

extension_point = ExtensionPoint("webapp")
extension = Extension("webapp")


class Specs:
    @staticmethod
    @extension_point
    def register_routes(router: APIRouter) -> None:
        """Attach endpoints to the shared router."""

A plugin

A plugin is any object whose method carries @extension. It adds routes to the router it is handed - it never imports or edits the host app:

class GreetingPlugin:
    @extension
    def register_routes(self, router: APIRouter) -> None:
        @router.get("/hello/{name}")
        def hello(name: str) -> dict[str, str]:
            return {"message": f"hello {name}"}

Assembling the app

The host registers its plugins, fires the hook once over a shared router, and mounts it. Adding a feature later means registering one more plugin here (or, with entry-point discovery, installing a package):

def build_app(*plugins: object) -> FastAPI:
    pm = PluginManager("webapp")
    pm.add_extension_points(Specs)
    for plugin in plugins or (HealthPlugin(), GreetingPlugin()):
        pm.register(plugin)
    router = APIRouter()
    pm.caller(Specs.register_routes)(router=router)
    app = FastAPI()
    app.include_router(router)
    return app

Because build_app takes its plugins as arguments, tests assemble an app from exactly the plugins under test:

from fastapi.testclient import TestClient

def test_routes():
    client = TestClient(build_app())
    assert client.get("/health").json() == {"status": "ok"}
    assert client.get("/hello/Ada").json() == {"message": "hello Ada"}

Why route through a hook

  • No edits to the core. New endpoints arrive with a plugin, so the app file stays stable as features grow.
  • Entry-point discovery. Swap pm.register(...) for pm.load_entrypoints("webapp") and any installed distribution advertising the webapp group contributes routes - the cross-package pattern shown in entry-point discovery.
  • Testable in isolation. Each plugin is a plain object; assemble an app from one plugin and exercise just its routes.