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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c05cb689ae8941be41dfcbd08fa22d78c52969dc2e1c6f1183af13edb97dfbef
|
|
| MD5 |
0da9b79c05b6d75bef6e6c5a67bf04f1
|
|
| BLAKE2b-256 |
c5a9417a9c2661125d1655ef934f9db6f721df1017c2fb7c6dd2bea12b69d73a
|
File details
Details for the file fastapi_canon-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fastapi_canon-0.1.0-py3-none-any.whl
- Upload date:
- Size: 24.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8531b421b3396b9bf578f0156c1cad22148dd477b17b4d4febaf915531f89957
|
|
| MD5 |
ad2c264179a93eda05b3e90d9dbdc9b0
|
|
| BLAKE2b-256 |
fe0634b6b28fdf6582fbabebc03d07e0aa322377ba4675da8cdf3f8b571ab7a5
|