Skip to content

Proposal: Extension Mechanism for FastAPI-Boilerplate #221

@carlosplanchon

Description

@carlosplanchon

Proposal: Extension Mechanism for FastAPI-Boilerplate

(with a structurally isomorphic design to the core)

1. Summary

This proposal defines a lightweight, explicit extension model for FastAPI-Boilerplate.

Goal: allow optional features (SSO, payments, analytics, notifications, etc.) to be shipped as separate Python packages ("extensions") that integrate cleanly into a FastAPI-Boilerplate project without modifying the core.

Key design principle:

Extensions are structurally isomorphic to built-in features of the boilerplate.
They use the same layers (settings -> dependencies -> routers -> optional models/middleware),
exposed in a separate package.

Concretely:

  • An extension provides a Settings mixin (Pydantic BaseSettings),
  • exposes a single registration function register_extension(app, settings),
  • and may optionally define models, migrations, middleware, and lifespan hooks.

A project opts into an extension by:

  • mixing the extension’s Settings into its main Settings,
  • calling register_extension in the application factory or startup code.

This keeps the boilerplate simple by default, while making it easy to build a small ecosystem of reusable, plug-and-play features.


2. Design Rationale: Structural Isomorphism with the Core

2.1. The idea

FastAPI-Boilerplate already organizes functionality in a clear layered structure:

  • Configuration via composed Settings classes (AppSettings, PostgresSettings, CryptSettings, ...)
  • HTTP layer via routers mounted on a main API router.
  • Dependency injection via reusable dependencies.
  • Infrastructure via models, CRUD utilities, middleware, background workers, etc.

The proposed extension model is intentionally isomorphic to this structure:

  • Extensions have their own XxxExtensionSettings that can be mixed into Settings.
  • Extensions expose routers that are included with app.include_router.
  • Extensions may have their own dependencies, models, migrations, middleware, and lifespan logic, using the same patterns as the core.

In other words:

An extension is "a feature module that lives outside the repo"
but behaves and integrates like a first-class, internal module.

2.2. Mapping core < - > extension

Core concept Extension concept Preserved structure
AppSettings / PostgresSettings MyExtensionSettings Settings composition via multiple inheritance
api_router.include_router(...) app.include_router(extension_router) Same router inclusion pattern
dependencies.py extension.dependencies Same DI patterns
models.py + Alembic extension.models + optional migrations Same persistence semantics
middleware in app setup extension.middleware Same middleware chain
lifespan / startup handlers extension.lifecycle Same startup/shutdown wiring

This structural isomorphism has a few benefits:

  • Predictable mental model: extension code "feels like" core code.
  • Lower cognitive load: extension authors follow familiar patterns.
  • Composability: future tooling (e.g. extension discovery, diagnostics) can rely on this shape.
  • Stability: if the core stabilizes a small "public surface" for extensions, there’s a clear contract.

2.3. Invariants extensions should preserve

To maintain this isomorphism, extensions are encouraged to respect the following invariants:

  1. Configuration invariant

    • There is a single project Settings object.
    • Extensions provide mixins and do not create their own, separate global settings objects.
  2. HTTP semantics invariant

    • Extensions use the same HTTP conventions as the core (status codes, error handling style, dependency patterns).
    • They integrate through routers rather than creating separate FastAPI apps.
  3. Security invariant (when applicable)

    • Extensions that deal with auth should integrate with the project’s existing user model and token machinery, rather than bypass it.
    • They may wrap those utilities, but should not create parallel, conflicting security subsystems.
  4. Dependency injection invariant

    • Extensions rely on the same DI approach (FastAPI dependencies) instead of introducing new DI frameworks.

These are recommendations rather than hard constraints, but they keep the mental model consistent and make extensions feel "native".


3. Goals and Non-Goals

3.1. Goals

  1. Explicit opt-in
    No magic discovery. The project owner explicitly chooses which extensions to use.

  2. Minimal contract
    Define a small interface for extensions that works for 80–90% of use cases.

  3. Structural isomorphism
    Extensions mirror the same structural layers as core features:

    • Settings -> Dependencies -> Routers -> optional Models/Middleware/Lifespan.
  4. Library-friendly
    Make it practical to publish extensions as pip packages (e.g. fastapi-boilerplate-sso).

  5. Backwards compatible
    Existing projects can ignore this mechanism and continue as-is.

3.2. Non-Goals

  • No automatic extension discovery or plugin registry (for now).
  • No enforced Alembic strategy (both "models-only" and "extension migrations" are allowed).
  • Not tied to any specific feature (SSO is just a motivating example).

4. Concept: What Is an Extension?

Definition:

A FastAPI-Boilerplate extension is a separate Python package that adds feature(s) to a project by:

  • providing a BaseSettings mixin, and
  • exposing a register_extension(app, settings) function that wires routes / middleware / hooks into the app,
  • using the same structural layering as the core (isomorphic design).

5. Extension Levels (Lightweight vs Stateful)

To keep things pragmatic, the spec distinguishes two levels. Both are structurally isomorphic to internal modules; they only differ in how many layers they use.

5.1. Level 1 – Lightweight Extension

Typical examples:
webhook handlers, feature flags endpoints, wrappers for external APIs, health/metrics endpoints.

MUST provide:

  • A XxxExtensionSettings(BaseSettings) mixin.

  • A register_extension(app, settings) function:

    • often includes routers,
    • may also add middleware or dependencies.

MAY provide:

  • Pydantic schemas.
  • Shared dependencies.

No requirement to touch the database or Alembic.

5.2. Level 2 – Stateful Extension

Typical examples:
SSO providers, subscription billing, notification center, audit log, etc.

Includes everything from Level 1 plus optionally:

  • SQLAlchemy models.
  • CRUD helpers.
  • Alembic migrations.
  • Middleware.
  • Lifespan hooks / startup logic.

The design remains isomorphic: a Level 2 extension is essentially "another feature module" with the same internals as a built-in one, but published as a library.


6. Minimal Extension Contract

Extensions should expose a single entry point:

# my_extension/__init__.py

from fastapi import FastAPI
from .settings import MyExtensionSettings

def register_extension(app: FastAPI, settings: MyExtensionSettings) -> None:
    """
    Register this extension into a FastAPI-Boilerplate app.

    Assumes `settings` is the project's Settings instance,
    which inherits from MyExtensionSettings.
    """
    ...

6.1. Settings Mixins

Each extension defines its own BaseSettings mixin, which the project composes into its main Settings:

# my_extension/settings.py

from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field, SecretStr

class MyExtensionSettings(BaseSettings):
    """Settings for MyExtension."""

    model_config = SettingsConfigDict(
        env_file=".env",
        extra="ignore",
    )

    MY_EXTENSION_ENABLED: bool = Field(default=False)
    MY_EXTENSION_API_KEY: SecretStr | None = Field(default=None)

Project integration:

# app/core/config.py

from my_extension.settings import MyExtensionSettings

class Settings(
    AppSettings,
    PostgresSettings,
    CryptSettings,
    MyExtensionSettings,  # <- extension mixin (isomorphic to other settings)
):
    pass

6.2. Router Inclusion

Most extensions will expose a router, just like first-party modules:

# my_extension/router.py

from fastapi import APIRouter, Depends
from typing import Annotated
from app.core.config import Settings
from app.core.dependencies import get_settings  # or equivalent

router = APIRouter(
    prefix="/my-extension",
    tags=["my-extension"],
)

@router.get("/status")
async def get_status(
    settings: Annotated[Settings, Depends(get_settings)],
):
    return {"enabled": settings.MY_EXTENSION_ENABLED}

Wiring it in register_extension:

# my_extension/__init__.py

from fastapi import FastAPI
from app.core.config import Settings
from .settings import MyExtensionSettings
from .router import router

def register_extension(app: FastAPI, settings: Settings) -> None:
    if not getattr(settings, "MY_EXTENSION_ENABLED", False):
        return

    app.include_router(router)

Project usage:

# app/main.py or app/core/setup.py

from fastapi import FastAPI
from my_extension import register_extension as register_my_extension

def create_application(...) -> FastAPI:

    # core wiring...

    # extension wiring (same pattern, isomorphic)
    register_my_extension(app, settings)

    return application

! This last part should be carefully reviewed.


7. Recommended Extension Package Layout

Not mandatory, but recommended to keep the ecosystem consistent:

my-fastapi-extension/
├── pyproject.toml
├── README.md
├── my_extension/
│   ├── __init__.py          # exposes: Settings mixin + register_extension
│   ├── settings.py          # MyExtensionSettings
│   ├── router.py            # API routers (Level 1)
│   ├── dependencies.py      # optional FastAPI dependencies
│   ├── models.py            # optional SQLAlchemy models (Level 2)
│   ├── crud.py              # optional CRUD helpers
│   ├── middleware.py        # optional middleware
│   ├── lifecycle.py         # optional lifespan/startup hooks
│   └── migrations/          # optional Alembic migration files (Level 2)
└── tests/
    └── ...

This mirrors the internal structure commonly used within the boilerplate.


8. Optional Integration Points (Isomorphic Layers)

These are optional but follow the same layering as the core.

8.1. Middleware

# my_extension/middleware.py

from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request

class MyExtensionMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # Custom logic
        response = await call_next(request)
        return response

Registered inside register_extension:

from .middleware import MyExtensionMiddleware

def register_extension(app: FastAPI, settings: Settings) -> None:
    if settings.MY_EXTENSION_ENABLED:
        app.add_middleware(MyExtensionMiddleware)

8.2. Lifespan / Startup Hooks

# my_extension/lifecycle.py

from contextlib import asynccontextmanager
from fastapi import FastAPI

@asynccontextmanager
async def my_extension_lifespan(app: FastAPI):
    # Startup
    await initialize_stuff()
    try:
        yield
    finally:
        # Shutdown
        await cleanup_stuff()

The project may combine its own lifespan with the extension’s lifespan context, preserving the same structural pattern as internal lifespan composition.


9. Database Models and Migrations

Extensions that need DB tables can choose between:

9.1. Minimal strategy (recommended for now)

  • Extension defines models only.
  • Project owner runs alembic revision --autogenerate after including the extension’s models in the metadata.
  • Keeps Alembic config simple (single migration tree).

10. Backwards Compatibility

  • Existing projects are unaffected.
  • The extension model is purely additive.
  • No changes to the core’s public API are strictly required; it only standardizes how external code should integrate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions