Recipes¶
Ready-made patterns for common tasks.
A shared tuple of auth errors¶
Token/auth errors are usually the same across many routes. Factor them into a tuple and unpack them with the marker:
from typing import Final
TOKEN_ERRORS: Final = (RequiredTokenError, InvalidTokenError, WrongTokenTypeError)
@router.get("/me")
def me(user: Annotated[User, Depends(current_user)]) -> Annotated[User, Raises[*TOKEN_ERRORS]]: ...
All three codes on 401 automatically become a discriminated oneOf union.
Dependency errors without manual declaration¶
If the auth errors live in dependencies (Depends), turn on auto=True — and don't declare them at all:
router = with_errors(APIRouter(), auto=True)
def current_user(token: Annotated[str, Depends(oauth)]) -> User:
if invalid(token):
raise InvalidTokenError("bad token") # found automatically
...
@router.get("/me")
def me(user: Annotated[User, Depends(current_user)]) -> User:
return user # the 401 from the dependency lands in responses by itself
The get_or_404 factory¶
A common pattern is a shared helper that raises the class passed to it:
def get_or_404[M](value: M | None, *, error: type[BaseError[Any]]) -> M:
if value is None:
raise error("not found")
return value
@router.get("/items/{item_id}")
def get_item(item_id: int) -> Annotated[Item, Raises[NotFoundError]]:
item = get_or_404(repo.find(item_id), error=NotFoundError)
return item
The walker (auto and check_raises) understands this factory: the error class passed as a call argument counts as potentially raised.
Testing error contracts¶
One line in a test guarantees the declarations haven't drifted from the code:
from fastapi_typed_errors import check_raises
def test_error_contracts() -> None:
assert check_raises(app).ok
The full response body is easy to check too, via TestClient:
def test_not_found() -> None:
response = TestClient(app).get("/items/0")
assert response.status_code == 404
assert response.json() == {"code": "NOT_FOUND", "detail": "gone"}
The whole application¶
Wrap the application's router so that app.get(...) decorators understand Raises too:
app = FastAPI()
app.add_exception_handler(BaseError, handle_base_error)
with_errors(app.router, auto=True)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
Next: Limitations — what is out of scope and why.