-
Notifications
You must be signed in to change notification settings - Fork 238
Description
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_extensionin 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
Settingsclasses (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
XxxExtensionSettingsthat can be mixed intoSettings. - 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:
-
Configuration invariant
- There is a single project
Settingsobject. - Extensions provide mixins and do not create their own, separate global settings objects.
- There is a single project
-
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.
-
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.
-
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
-
Explicit opt-in
No magic discovery. The project owner explicitly chooses which extensions to use. -
Minimal contract
Define a small interface for extensions that works for 80–90% of use cases. -
Structural isomorphism
Extensions mirror the same structural layers as core features:- Settings -> Dependencies -> Routers -> optional Models/Middleware/Lifespan.
-
Library-friendly
Make it practical to publish extensions aspippackages (e.g.fastapi-boilerplate-sso). -
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
BaseSettingsmixin, 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)
):
pass6.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 responseRegistered 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 --autogenerateafter 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.