Skip to main content

fastapi-canon

fastapi-canon is an opinionated composition library for feature-oriented FastAPI applications. A feature groups its routers, Dishka providers, error contracts, exception handlers, and lifespan into one immutable value. The application installs an explicitly ordered set of those values at its composition root.

Dishka is a deliberate part of this canon, not an optional integration. fastapi-canon defines one dependency-injection approach: features contribute Dishka providers, and the composition builds and owns one shared Dishka container. Applications that choose another dependency-injection framework are outside the library's intended architecture.

from fastapi import APIRouter, FastAPI
from fastapi_canon import Composition, Feature

projects = APIRouter(prefix="/projects", tags=["projects"])


@projects.get("")
async def list_projects() -> list[str]:
    return []


project_feature = Feature(routers=[projects])

app = Composition(project_feature).apply(FastAPI())

Contributions

Every contribution is optional:

feature = Feature(
    routers=[router],
    providers=[provider],
    errors=feature_errors,
    exception_handlers=[handler_spec],
    lifespan=feature_lifespan,
)

Mutable sequences passed to Feature are copied to tuples. Installing features preserves declaration order for routes and startup. Shutdown runs in reverse order, including cleanup of features that started before a later feature failed.

Dishka

Dishka is a required runtime dependency and the canonical dependency-injection mechanism. Provider instances from every feature are validated together and used to build one AsyncContainer. Applying the composition configures Dishka's FastAPI middleware and closes the container during application shutdown. Dishka exposes the container as app.state.dishka_container.

Errors

Error contracts are implemented directly by fastapi-canon; no separate error library is required. Each feature may expose one ErrorRegistry. Registries are merged and installed once, so runtime RFC 9457 Problem Details and OpenAPI use the same definitions:

from fastapi_canon import Composition, Error, ErrorOptions, ErrorRegistry, Feature


class ProjectNotFound(Exception):
    pass


project_not_found = Error(
    ProjectNotFound,
    status=404,
    code="project_not_found",
    title="Project not found",
    detail=lambda error: str(error),
)

project_errors = ErrorRegistry(
    name="projects",
    errors=[project_not_found],
)

project_feature = Feature(
    routers=[projects],
    errors=project_errors,
)

app = Composition(
    project_feature,
    errors=ErrorOptions(
        type_base="https://api.example.com/problems",
    ),
).apply(FastAPI())

Declare endpoint responses from the same registry used at runtime:

@projects.get(
    "/{project_id}",
    responses=project_errors.responses(project_not_found),
)
async def get_project(project_id: str) -> dict[str, str]:
    raise ProjectNotFound(project_id)

OpenAPI representation

Every declared error becomes a reusable schema in components.schemas. The corresponding operation references it as an application/problem+json response:

paths:
  /projects/{project_id}:
    get:
      responses:
        "404":
          description: Project not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProjectNotFoundProblem"

components:
  schemas:
    ProjectNotFoundProblem:
      type: object
      required: [type, title, status, code]
      properties:
        type:
          type: string
          const: https://api.example.com/problems/project_not_found
        title:
          type: string
          const: Project not found
        status:
          type: integer
          const: 404
        code:
          type: string
          const: project_not_found
        detail:
          type: [string, "null"]

Typed extension fields and documented response headers are added to that same schema and response. If multiple errors share one status code, the response uses oneOf with code as its discriminator. When validation normalization is enabled, FastAPI's default 422 response is replaced by RequestValidationProblem using the same media type.

When all local registries already share a type_base, it is inferred. The ErrorOptions settings include_validation_error, include_http_exceptions, and include_unhandled_error are passed to the integrated error engine and default to True.

Use ExceptionHandlerSpec for a deliberately custom Starlette/FastAPI handler:

from fastapi_canon import ExceptionHandlerSpec

feature = Feature(
    exception_handlers=[ExceptionHandlerSpec(DomainError, domain_error_handler)],
)

Installation guarantees

  • Feature order is explicit and deterministic.
  • Reinstalling the exact same feature objects with the same options is a no-op.
  • A different second installation is rejected.
  • Duplicate routers, providers, handlers, and error collisions fail during configuration.
  • Known configuration errors are validated against a temporary application before the real application is changed.
  • Provider-backed features must be installed before the application starts.
  • Disabling a feature means omitting it from Composition, which removes all of its contributions together.

Configuration failures raise FeatureConfigurationError.

Requirements

  • CPython 3.12, 3.13, or 3.14
  • FastAPI 0.115 or newer, below 1.0
  • Dishka 1.10 or newer, below 2.0
  • Pydantic 2.9 or newer, below 3.0

Development

Install the development dependencies and run the quality gates:

uv sync --all-groups
uv run pre-commit install --config .pre-commit-config.yml
uv run pre-commit run --config .pre-commit-config.yml --all-files
uv run ruff format --check .
uv run ruff check .
uv run mypy
uv run pytest
uv build

Showcase

See examples/showcase for a runnable two-feature FastAPI application. It keeps error contracts alongside their feature routes and merges them once at the composition root.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastapi_canon-0.1.0.tar.gz (83.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_canon-0.1.0-py3-none-any.whl (24.8 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_canon-0.1.0.tar.gz.

File metadata

  • Download URL: fastapi_canon-0.1.0.tar.gz
  • Upload date:
  • Size: 83.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.2

File hashes

Hashes for fastapi_canon-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c05cb689ae8941be41dfcbd08fa22d78c52969dc2e1c6f1183af13edb97dfbef
MD5 0da9b79c05b6d75bef6e6c5a67bf04f1
BLAKE2b-256 c5a9417a9c2661125d1655ef934f9db6f721df1017c2fb7c6dd2bea12b69d73a

See more details on using hashes here.

File details

Details for the file fastapi_canon-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_canon-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8531b421b3396b9bf578f0156c1cad22148dd477b17b4d4febaf915531f89957
MD5 ad2c264179a93eda05b3e90d9dbdc9b0
BLAKE2b-256 fe0634b6b28fdf6582fbabebc03d07e0aa322377ba4675da8cdf3f8b571ab7a5

See more details on using hashes here.

Release history Release notifications | RSS feed

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page