[{"data":1,"prerenderedAt":2297},["ShallowReactive",2],{"nav":3,"page-\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fhttpexception-vs-custom-exception-classes\u002F":580,"surround-\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fhttpexception-vs-custom-exception-classes\u002F":2296},[4,186,386],{"title":5,"path":6,"stem":7,"children":8},"Advanced Pydantic Validation Serialization","\u002Fadvanced-pydantic-validation-serialization","advanced-pydantic-validation-serialization",[9,12,42,66,90,114,150,174],{"title":10,"path":6,"stem":11},"Advanced Pydantic Validation and Serialization","advanced-pydantic-validation-serialization\u002Findex",{"title":13,"path":14,"stem":15,"children":16},"Custom Validators and Field Constraints in Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Findex",[17,18,24,30,36],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":22},"Before, After and Wrap Validators in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fbefore-after-and-wrap-validators","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fbefore-after-and-wrap-validators\u002Findex",[23],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":28},"Creating Reusable Custom Validators in Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcreating-reusable-custom-validators-in-pydantic","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcreating-reusable-custom-validators-in-pydantic\u002Findex",[29],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":34},"Cross-Field Validation Patterns in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcross-field-validation-patterns","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcross-field-validation-patterns\u002Findex",[35],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":40},"Pydantic v2 Async Custom Validator: What to Do Instead","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fpydantic-v2-async-custom-validator","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fpydantic-v2-async-custom-validator\u002Findex",[41],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":46},"JSON Schema Customization in Pydantic and FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Findex",[47,48,54,60],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":52},"Customizing OpenAPI Schema Generation in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fcustomizing-openapi-schema-generation-in-fastapi","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fcustomizing-openapi-schema-generation-in-fastapi\u002Findex",[53],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":58},"Discriminated Unions in OpenAPI with Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fdiscriminated-unions-in-openapi","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fdiscriminated-unions-in-openapi\u002Findex",[59],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":64},"Examples in the OpenAPI Schema with FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fexamples-in-openapi-schema","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fexamples-in-openapi-schema\u002Findex",[65],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":70},"Nested Model Serialization in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Findex",[71,72,78,84],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":76},"Excluding Fields Per Endpoint in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fexcluding-fields-per-endpoint","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fexcluding-fields-per-endpoint\u002Findex",[77],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":82},"Handling Deeply Nested JSON Models Efficiently","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fhandling-deeply-nested-json-models-efficiently","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fhandling-deeply-nested-json-models-efficiently\u002Findex",[83],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87,"children":88},"Self-Referencing and Recursive Models in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fself-referencing-and-recursive-models","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fself-referencing-and-recursive-models\u002Findex",[89],{"title":85,"path":86,"stem":87},{"title":91,"path":92,"stem":93,"children":94},"Performance Optimization for Pydantic Models in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Findex",[95,96,102,108],{"title":91,"path":92,"stem":93},{"title":97,"path":98,"stem":99,"children":100},"model_construct and When to Skip Validation in Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Fmodel-construct-when-to-skip-validation","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Fmodel-construct-when-to-skip-validation\u002Findex",[101],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":106},"Pydantic Model Serialization Performance in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Fpydantic-model-serialization-performance","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Fpydantic-model-serialization-performance\u002Findex",[107],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":112},"TypeAdapter for Non-Model Types in Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Ftypeadapter-for-non-model-types","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Ftypeadapter-for-non-model-types\u002Findex",[113],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":118},"Pydantic V2 Migration Guide for FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Findex",[119,120,126,132,138,144],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":124},"Migrate @validator to @field_validator in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmigrate-validator-to-field-validator","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmigrate-validator-to-field-validator\u002Findex",[125],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":130},"Migrating from Pydantic v1 to v2 Without Breaking APIs","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmigrating-from-pydantic-v1-to-v2-without-breaking-apis","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmigrating-from-pydantic-v1-to-v2-without-breaking-apis\u002Findex",[131],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":136},"model_config vs class Config in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmodel-config-vs-class-config","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmodel-config-vs-class-config\u002Findex",[137],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":142},"Replacing json_encoders with field_serializer in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Freplacing-json-encoders-with-field-serializer","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Freplacing-json-encoders-with-field-serializer\u002Findex",[143],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":148},"Migrating @root_validator to @model_validator in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Froot-validator-to-model-validator","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Froot-validator-to-model-validator\u002Findex",[149],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":154},"Request Validation Patterns in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns","advanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Findex",[155,156,162,168],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":160},"Optional vs Nullable Fields in Pydantic and FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Foptional-vs-nullable-fields","advanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Foptional-vs-nullable-fields\u002Findex",[161],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":166},"Query, Path and Body Parameter Validation in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fquery-path-and-body-parameter-validation","advanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fquery-path-and-body-parameter-validation\u002Findex",[167],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":172},"Validating File Uploads and Forms in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fvalidating-file-uploads-and-forms","advanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fvalidating-file-uploads-and-forms\u002Findex",[173],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":178},"Type Hinting and IDE Integration in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration","advanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration\u002Findex",[179,180],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":184},"Annotated Dependencies and Reusable Types in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration\u002Fannotated-dependencies-and-reusable-types","advanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration\u002Fannotated-dependencies-and-reusable-types\u002Findex",[185],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":190},"Async Background Tasks Observability","\u002Fasync-background-tasks-observability","async-background-tasks-observability",[191,194,224,254,284,308,338,362],{"title":192,"path":188,"stem":193},"Async, Background Tasks, and Observability in FastAPI","async-background-tasks-observability\u002Findex",{"title":195,"path":196,"stem":197,"children":198},"Async Correctness and Concurrency in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Findex",[199,200,206,212,218],{"title":195,"path":196,"stem":197},{"title":201,"path":202,"stem":203,"children":204},"Concurrent Requests with asyncio.gather in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Fconcurrent-requests-with-asyncio-gather","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Fconcurrent-requests-with-asyncio-gather\u002Findex",[205],{"title":201,"path":202,"stem":203},{"title":207,"path":208,"stem":209,"children":210},"FastAPI async def vs def: Performance and When to Use Each","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffastapi-async-def-vs-def-performance","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffastapi-async-def-vs-def-performance\u002Findex",[211],{"title":207,"path":208,"stem":209},{"title":213,"path":214,"stem":215,"children":216},"Fixing Blocking Calls in Async FastAPI Routes","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffixing-blocking-calls-in-async-routes","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffixing-blocking-calls-in-async-routes\u002Findex",[217],{"title":213,"path":214,"stem":215},{"title":219,"path":220,"stem":221,"children":222},"Running Sync Code in a Threadpool in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Frunning-sync-code-in-a-threadpool","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Frunning-sync-code-in-a-threadpool\u002Findex",[223],{"title":219,"path":220,"stem":221},{"title":225,"path":226,"stem":227,"children":228},"Async Database Sessions in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions","async-background-tasks-observability\u002Fasync-database-sessions\u002Findex",[229,230,236,242,248],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":234},"Async SQLAlchemy Session per Request in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Fasync-sqlalchemy-session-per-request","async-background-tasks-observability\u002Fasync-database-sessions\u002Fasync-sqlalchemy-session-per-request\u002Findex",[235],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":240},"Fixing asyncpg Connection Pool Exhaustion in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ffixing-asyncpg-pool-exhaustion","async-background-tasks-observability\u002Fasync-database-sessions\u002Ffixing-asyncpg-pool-exhaustion\u002Findex",[241],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":246},"Testing with Async Database Fixtures in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ftesting-with-async-database-fixtures","async-background-tasks-observability\u002Fasync-database-sessions\u002Ftesting-with-async-database-fixtures\u002Findex",[247],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":252},"Transaction Management and Rollback in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ftransaction-management-and-rollback","async-background-tasks-observability\u002Fasync-database-sessions\u002Ftransaction-management-and-rollback\u002Findex",[253],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":258},"Background Task Processing in FastAPI","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing","async-background-tasks-observability\u002Fbackground-task-processing\u002Findex",[259,260,266,272,278],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":264},"FastAPI BackgroundTasks vs Celery vs ARQ","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Ffastapi-backgroundtasks-vs-celery-vs-arq","async-background-tasks-observability\u002Fbackground-task-processing\u002Ffastapi-backgroundtasks-vs-celery-vs-arq\u002Findex",[265],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":270},"Retry and Idempotency for FastAPI Background Tasks","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Fretry-and-idempotency-for-tasks","async-background-tasks-observability\u002Fbackground-task-processing\u002Fretry-and-idempotency-for-tasks\u002Findex",[271],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":276},"Running ARQ Workers with FastAPI","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Frunning-arq-workers-with-fastapi","async-background-tasks-observability\u002Fbackground-task-processing\u002Frunning-arq-workers-with-fastapi\u002Findex",[277],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":282},"When FastAPI BackgroundTasks Silently Fails","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Fwhen-backgroundtasks-silently-fails","async-background-tasks-observability\u002Fbackground-task-processing\u002Fwhen-backgroundtasks-silently-fails\u002Findex",[283],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":288},"Caching Strategies in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies","async-background-tasks-observability\u002Fcaching-strategies\u002Findex",[289,290,296,302],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":294},"Cache Invalidation Patterns in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies\u002Fcache-invalidation-patterns-in-fastapi","async-background-tasks-observability\u002Fcaching-strategies\u002Fcache-invalidation-patterns-in-fastapi\u002Findex",[295],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":300},"Caching Dependency Results in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies\u002Fcaching-dependency-results","async-background-tasks-observability\u002Fcaching-strategies\u002Fcaching-dependency-results\u002Findex",[301],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":306},"Redis Response Caching in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies\u002Fredis-response-caching-in-fastapi","async-background-tasks-observability\u002Fcaching-strategies\u002Fredis-response-caching-in-fastapi\u002Findex",[307],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":312},"Observability and Tracing in FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing","async-background-tasks-observability\u002Fobservability-and-tracing\u002Findex",[313,314,320,326,332],{"title":309,"path":310,"stem":311},{"title":315,"path":316,"stem":317,"children":318},"Correlating Logs, Traces and Errors in FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Fcorrelating-logs-traces-and-errors","async-background-tasks-observability\u002Fobservability-and-tracing\u002Fcorrelating-logs-traces-and-errors\u002Findex",[319],{"title":315,"path":316,"stem":317},{"title":321,"path":322,"stem":323,"children":324},"Instrumenting FastAPI with OpenTelemetry","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Finstrumenting-fastapi-with-opentelemetry","async-background-tasks-observability\u002Fobservability-and-tracing\u002Finstrumenting-fastapi-with-opentelemetry\u002Findex",[325],{"title":321,"path":322,"stem":323},{"title":327,"path":328,"stem":329,"children":330},"Prometheus Metrics for FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Fprometheus-metrics-for-fastapi","async-background-tasks-observability\u002Fobservability-and-tracing\u002Fprometheus-metrics-for-fastapi\u002Findex",[331],{"title":327,"path":328,"stem":329},{"title":333,"path":334,"stem":335,"children":336},"Structured JSON Logging with Request IDs in FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Fstructured-json-logging-with-request-ids","async-background-tasks-observability\u002Fobservability-and-tracing\u002Fstructured-json-logging-with-request-ids\u002Findex",[337],{"title":333,"path":334,"stem":335},{"title":339,"path":340,"stem":341,"children":342},"Rate Limiting and Throttling in FastAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling","async-background-tasks-observability\u002Frate-limiting-throttling\u002Findex",[343,344,350,356],{"title":339,"path":340,"stem":341},{"title":345,"path":346,"stem":347,"children":348},"FastAPI Rate Limiting with Redis and SlowAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Ffastapi-rate-limiting-with-redis-slowapi","async-background-tasks-observability\u002Frate-limiting-throttling\u002Ffastapi-rate-limiting-with-redis-slowapi\u002Findex",[349],{"title":345,"path":346,"stem":347},{"title":351,"path":352,"stem":353,"children":354},"Per-User Token Bucket Throttling in FastAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Fper-user-token-bucket-throttling","async-background-tasks-observability\u002Frate-limiting-throttling\u002Fper-user-token-bucket-throttling\u002Findex",[355],{"title":351,"path":352,"stem":353},{"title":357,"path":358,"stem":359,"children":360},"Rate Limit Headers and 429 Responses in FastAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Frate-limit-headers-and-429-responses","async-background-tasks-observability\u002Frate-limiting-throttling\u002Frate-limit-headers-and-429-responses\u002Findex",[361],{"title":357,"path":358,"stem":359},{"title":363,"path":364,"stem":365,"children":366},"Testing FastAPI Applications","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications","async-background-tasks-observability\u002Ftesting-fastapi-applications\u002Findex",[367,368,374,380],{"title":363,"path":364,"stem":365},{"title":369,"path":370,"stem":371,"children":372},"Mocking External Services in FastAPI Tests","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Fmocking-external-services-in-tests","async-background-tasks-observability\u002Ftesting-fastapi-applications\u002Fmocking-external-services-in-tests\u002Findex",[373],{"title":369,"path":370,"stem":371},{"title":375,"path":376,"stem":377,"children":378},"TestClient vs httpx AsyncClient in FastAPI","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftestclient-vs-httpx-asyncclient","async-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftestclient-vs-httpx-asyncclient\u002Findex",[379],{"title":375,"path":376,"stem":377},{"title":381,"path":382,"stem":383,"children":384},"Testing Async FastAPI Endpoints with pytest-asyncio","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftesting-async-endpoints-with-pytest-asyncio","async-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftesting-async-endpoints-with-pytest-asyncio\u002Findex",[385],{"title":381,"path":382,"stem":383},{"title":387,"path":388,"stem":389,"children":390},"Core Architecture Routing Patterns","\u002Fcore-architecture-routing-patterns","core-architecture-routing-patterns",[391,394,412,436,472,496,526,556],{"title":392,"path":388,"stem":393},"FastAPI Core Architecture and Routing Patterns","core-architecture-routing-patterns\u002Findex",{"title":395,"path":396,"stem":397,"children":398},"Application Factory Patterns in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fapplication-factory-patterns","core-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Findex",[399,400,406],{"title":395,"path":396,"stem":397},{"title":401,"path":402,"stem":403,"children":404},"FastAPI App Factory Pattern for Testing and Deployment","\u002Fcore-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Ffastapi-app-factory-pattern-for-testing-and-deployment","core-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Ffastapi-app-factory-pattern-for-testing-and-deployment\u002Findex",[405],{"title":401,"path":402,"stem":403},{"title":407,"path":408,"stem":409,"children":410},"Lifespan Events vs Startup and Shutdown in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Flifespan-events-vs-startup-shutdown","core-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Flifespan-events-vs-startup-shutdown\u002Findex",[411],{"title":407,"path":408,"stem":409},{"title":413,"path":414,"stem":415,"children":416},"Configuration Management in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management","core-architecture-routing-patterns\u002Fconfiguration-management\u002Findex",[417,418,424,430],{"title":413,"path":414,"stem":415},{"title":419,"path":420,"stem":421,"children":422},"Managing Environment Variables with Pydantic Settings","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management\u002Fmanaging-environment-variables-with-pydantic-settings","core-architecture-routing-patterns\u002Fconfiguration-management\u002Fmanaging-environment-variables-with-pydantic-settings\u002Findex",[423],{"title":419,"path":420,"stem":421},{"title":425,"path":426,"stem":427,"children":428},"Pydantic Settings vs Dynaconf vs python-decouple","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management\u002Fpydantic-settings-vs-dynaconf-vs-python-decouple","core-architecture-routing-patterns\u002Fconfiguration-management\u002Fpydantic-settings-vs-dynaconf-vs-python-decouple\u002Findex",[429],{"title":425,"path":426,"stem":427},{"title":431,"path":432,"stem":433,"children":434},"Secrets and .env Files Per Environment in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management\u002Fsecrets-and-env-files-per-environment","core-architecture-routing-patterns\u002Fconfiguration-management\u002Fsecrets-and-env-files-per-environment\u002Findex",[435],{"title":431,"path":432,"stem":433},{"title":437,"path":438,"stem":439,"children":440},"Dependency Injection Strategies in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Findex",[441,442,448,454,460,466],{"title":437,"path":438,"stem":439},{"title":443,"path":444,"stem":445,"children":446},"Best Practices for FastAPI Dependency Injection","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fbest-practices-for-fastapi-dependency-injection","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fbest-practices-for-fastapi-dependency-injection\u002Findex",[447],{"title":443,"path":444,"stem":445},{"title":449,"path":450,"stem":451,"children":452},"Dependency Caching and use_cache in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fdependency-caching-and-use-cache","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fdependency-caching-and-use-cache\u002Findex",[453],{"title":449,"path":450,"stem":451},{"title":455,"path":456,"stem":457,"children":458},"Fixing FastAPI Dependency Injection Circular Imports","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Ffastapi-dependency-injection-circular-import-fix","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Ffastapi-dependency-injection-circular-import-fix\u002Findex",[459],{"title":455,"path":456,"stem":457},{"title":461,"path":462,"stem":463,"children":464},"Overriding Dependencies in FastAPI Tests","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Foverriding-dependencies-in-tests","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Foverriding-dependencies-in-tests\u002Findex",[465],{"title":461,"path":462,"stem":463},{"title":467,"path":468,"stem":469,"children":470},"Yield Dependencies and Cleanup Order in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fyield-dependencies-and-cleanup-order","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fyield-dependencies-and-cleanup-order\u002Findex",[471],{"title":467,"path":468,"stem":469},{"title":473,"path":474,"stem":475,"children":476},"Error Handling and Global Exceptions in FastAPI","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Findex",[477,478,484,490],{"title":473,"path":474,"stem":475},{"title":479,"path":480,"stem":481,"children":482},"Customising Validation Error Responses in FastAPI","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fcustomising-validation-error-responses","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fcustomising-validation-error-responses\u002Findex",[483],{"title":479,"path":480,"stem":481},{"title":485,"path":486,"stem":487,"children":488},"Global Exception Handlers for Consistent API Responses","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fglobal-exception-handlers-for-consistent-api-responses","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fglobal-exception-handlers-for-consistent-api-responses\u002Findex",[489],{"title":485,"path":486,"stem":487},{"title":491,"path":492,"stem":493,"children":494},"HTTPException vs Custom Exception Classes in FastAPI","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fhttpexception-vs-custom-exception-classes","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fhttpexception-vs-custom-exception-classes\u002Findex",[495],{"title":491,"path":492,"stem":493},{"title":497,"path":498,"stem":499,"children":500},"Middleware Implementation in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Findex",[501,502,508,514,520],{"title":497,"path":498,"stem":499},{"title":503,"path":504,"stem":505,"children":506},"CORS Middleware Configuration in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fcors-middleware-configuration","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fcors-middleware-configuration\u002Findex",[507],{"title":503,"path":504,"stem":505},{"title":509,"path":510,"stem":511,"children":512},"Implementing Custom Middleware for Request Tracing","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fimplementing-custom-middleware-for-request-tracing","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fimplementing-custom-middleware-for-request-tracing\u002Findex",[513],{"title":509,"path":510,"stem":511},{"title":515,"path":516,"stem":517,"children":518},"Middleware Execution Order in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-execution-order","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-execution-order\u002Findex",[519],{"title":515,"path":516,"stem":517},{"title":521,"path":522,"stem":523,"children":524},"Middleware vs Dependencies: When to Use Which","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-vs-dependencies-when-to-use-which","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-vs-dependencies-when-to-use-which\u002Findex",[525],{"title":521,"path":522,"stem":523},{"title":527,"path":528,"stem":529,"children":530},"Modular Router Organization in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Findex",[531,532,538,544,550],{"title":527,"path":528,"stem":529},{"title":533,"path":534,"stem":535,"children":536},"APIRouter Prefix vs Sub-Application Mounting in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Fapirouter-prefix-vs-sub-application-mounting","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Fapirouter-prefix-vs-sub-application-mounting\u002Findex",[537],{"title":533,"path":534,"stem":535},{"title":539,"path":540,"stem":541,"children":542},"How to Structure Large FastAPI Projects for Scale","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Fhow-to-structure-large-fastapi-projects-for-scale","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Fhow-to-structure-large-fastapi-projects-for-scale\u002Findex",[543],{"title":539,"path":540,"stem":541},{"title":545,"path":546,"stem":547,"children":548},"Router Tags and OpenAPI Grouping in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Frouter-tags-and-openapi-grouping","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Frouter-tags-and-openapi-grouping\u002Findex",[549],{"title":545,"path":546,"stem":547},{"title":551,"path":552,"stem":553,"children":554},"Versioning APIs with FastAPI Routers","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Fversioning-apis-with-routers","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Fversioning-apis-with-routers\u002Findex",[555],{"title":551,"path":552,"stem":553},{"title":557,"path":558,"stem":559,"children":560},"The FastAPI Request\u002FResponse Lifecycle","\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle","core-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Findex",[561,562,568,574],{"title":557,"path":558,"stem":559},{"title":563,"path":564,"stem":565,"children":566},"How a Request Flows Through FastAPI","\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fhow-a-request-flows-through-fastapi","core-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fhow-a-request-flows-through-fastapi\u002Findex",[567],{"title":563,"path":564,"stem":565},{"title":569,"path":570,"stem":571,"children":572},"Response Model and Serialization Order","\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fresponse-model-and-serialization-order","core-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fresponse-model-and-serialization-order\u002Findex",[573],{"title":569,"path":570,"stem":571},{"title":575,"path":576,"stem":577,"children":578},"Streaming and File Responses in FastAPI","\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fstreaming-and-file-responses","core-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fstreaming-and-file-responses\u002Findex",[579],{"title":575,"path":576,"stem":577},{"id":581,"title":491,"body":582,"dateModified":2265,"datePublished":2265,"description":2266,"extension":2267,"faq":2268,"howto":2279,"meta":2280,"navigation":910,"path":492,"seo":2293,"stem":493,"type":2294,"__hash__":2295},"content\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fhttpexception-vs-custom-exception-classes\u002Findex.md",{"type":583,"value":584,"toc":2256},"minimark",[585,589,596,629,638,643,646,653,773,777,784,787,809,824,848,852,858,1349,1360,1363,1726,1729,1736,1739,1764,1778,1789,1798,1802,1805,1964,1967,2056,2062,2077,2081,2090,2105,2118,2127,2142,2149,2153,2162,2171,2185,2196,2217,2221,2252],[586,587,491],"h1",{"id":588},"httpexception-vs-custom-exception-classes-in-fastapi",[590,591,592],"p",{},[593,594,595],"strong",{},"Key takeaways:",[597,598,599,607,617,620,626],"ul",{},[600,601,602,606],"li",{},[603,604,605],"code",{},"HTTPException"," belongs at the edge; a domain exception belongs anywhere the business rule lives.",[600,608,609,610,612,613,616],{},"Give domain exceptions a ",[603,611,603],{}," and a ",[603,614,615],{},"status"," on the class, and register one handler for the base.",[600,618,619],{},"Starlette resolves handlers by walking the exception's MRO, so a base-class handler catches every subclass.",[600,621,622,623,625],{},"A service that raises ",[603,624,605],{}," cannot be reused by a worker, a CLI or a test without importing FastAPI.",[600,627,628],{},"Handlers do not fire for exceptions raised in background tasks or after a response has started streaming.",[590,630,631,632,637],{},"This guide is part of ",[633,634,636],"a",{"href":635},"\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002F","Error Handling and Global Exceptions",", which covers making error responses consistent; this page covers the decision that comes first — what to raise in the first place.",[639,640,642],"h2",{"id":641},"the-problem-this-solves","The Problem This Solves",[590,644,645],{},"A withdrawal fails because the balance is too low. Somewhere that fact has to become a 409 with a machine-readable code and enough context for the client to show a useful message. The question is where.",[590,647,648,649,652],{},"The path of least resistance is ",[603,650,651],{},"raise HTTPException(status_code=409, detail=\"Insufficient funds\")"," inside the function that checks the balance. It works, it is one line, and it quietly makes that function unusable outside a web request. The nightly reconciliation job that calls the same logic now depends on FastAPI, and the exception it catches carries an HTTP status code that means nothing in a cron job. Six months later the same rule is expressed twice, in two places, differently.",[654,655,661,665,669,676,680,691,696,701,706,710,713,718,722,724,727,731,735,739,741,744,747,751,754,756,759,763,768,770],"svg",{"viewBox":656,"role":657,"ariaLabel":658,"xmlns":659,"style":660},"0 0 720 300","img","Two error paths: a domain exception mapped by a handler, and HTTPException raised at the edge","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;max-width:720px;margin:2rem 0",[662,663,664],"title",{},"Domain exception versus HTTPException",[666,667,668],"desc",{},"On the left, the service layer raises a domain exception which crosses into a registered handler and becomes a structured 409 envelope. On the right, the path operation raises HTTPException directly and Starlette's built-in handler renders a detail body.",[670,671,675],"text",{"x":672,"y":673,"style":674},"175","26","text-anchor:middle;fill:#00796B;font:700 13px sans-serif","domain exception",[670,677,605],{"x":678,"y":673,"style":679},"545","text-anchor:middle;fill:currentColor;font:700 13px sans-serif",[681,682],"rect",{"x":683,"y":684,"width":685,"height":686,"rx":687,"fill":688,"stroke":689,"strokeWidth":690},"20","44","310","60","8","none","#00796B","2",[670,692,695],{"x":672,"y":693,"style":694},"70","text-anchor:middle;fill:#00796B;font:600 12px sans-serif","service layer — imports no FastAPI",[670,697,700],{"x":672,"y":698,"style":699},"90","text-anchor:middle;fill:currentColor;font:400 12px sans-serif","raise InsufficientFunds(...)",[681,702],{"x":703,"y":684,"width":685,"height":686,"rx":687,"fill":688,"stroke":704,"strokeWidth":705},"390","currentColor","1.5",[670,707,709],{"x":678,"y":693,"style":708},"text-anchor:middle;fill:currentColor;font:600 12px sans-serif","path operation — the edge",[670,711,712],{"x":678,"y":698,"style":699},"raise HTTPException(404, ...)",[714,715],"line",{"x1":672,"y1":716,"x2":672,"y2":717,"stroke":689,"strokeWidth":705},"104","136",[719,720],"polygon",{"points":721,"fill":689},"170,136 175,146 180,136",[714,723],{"x1":678,"y1":716,"x2":678,"y2":717,"stroke":704,"strokeWidth":705},[719,725],{"points":726,"fill":704},"540,136 545,146 550,136",[681,728],{"x":683,"y":729,"width":685,"height":730,"rx":687,"fill":688,"stroke":689,"strokeWidth":690},"148","56",[670,732,734],{"x":672,"y":733,"style":694},"172","your handler",[670,736,738],{"x":672,"y":737,"style":699},"192","exception_handler(DomainError)",[681,740],{"x":703,"y":729,"width":685,"height":730,"rx":687,"fill":688,"stroke":704,"strokeWidth":705},[670,742,743],{"x":678,"y":733,"style":708},"built-in handler",[670,745,746],{"x":678,"y":737,"style":699},"Starlette renders it",[714,748],{"x1":672,"y1":749,"x2":672,"y2":750,"stroke":689,"strokeWidth":705},"204","232",[719,752],{"points":753,"fill":689},"170,232 175,242 180,232",[714,755],{"x1":678,"y1":749,"x2":678,"y2":750,"stroke":704,"strokeWidth":705},[719,757],{"points":758,"fill":704},"540,232 545,242 550,232",[681,760],{"x":683,"y":761,"width":685,"height":762,"rx":687,"fill":688,"stroke":689,"strokeWidth":690},"244","46",[670,764,767],{"x":672,"y":765,"style":766},"272","text-anchor:middle;fill:#00796B;font:400 12px sans-serif","409 · error.code, message, context",[681,769],{"x":703,"y":761,"width":685,"height":762,"rx":687,"fill":688,"stroke":704,"strokeWidth":705},[670,771,772],{"x":678,"y":765,"style":699},"404 · detail",[639,774,776],{"id":775},"why-it-happens","Why It Happens",[590,778,779,780,783],{},"Starlette wraps your application in an exception middleware that sits outside the router. When a path operation raises, that middleware catches the exception and looks for a handler by walking ",[603,781,782],{},"type(exc).__mro__"," — the exception's class, then its base classes in order — and using the first one that has a registered handler.",[590,785,786],{},"Two consequences follow directly, and they are the whole basis of the pattern on this page.",[590,788,789,790,792,793,796,797,800,801,804,805,808],{},"First, ",[603,791,605],{}," is not special to the framework's core. FastAPI registers a default handler for it during app construction, exactly the way ",[603,794,795],{},"app.exception_handler(...)"," registers yours. It converts the exception into a ",[603,798,799],{},"JSONResponse"," with a ",[603,802,803],{},"detail"," key and copies across any ",[603,806,807],{},"headers"," you passed. Nothing stops you from replacing that handler, and nothing makes it more privileged than one of yours.",[590,810,811,812,815,816,819,820,823],{},"Second, because the lookup walks the MRO, registering a handler for a base class covers the entire hierarchy beneath it. One ",[603,813,814],{},"@app.exception_handler(DomainError)"," catches ",[603,817,818],{},"AccountNotFound",", ",[603,821,822],{},"InsufficientFunds",", and every exception you add next year, without touching the wiring. That is what makes a domain exception hierarchy cheap: the cost of a new error type is one class with two class attributes.",[590,825,826,827,830,831,834,835,838,839,842,843,847],{},"The lookup happens inside the middleware stack, which is also why the pattern has hard edges. A ",[603,828,829],{},"BackgroundTask"," runs after the response has been sent, so an exception there has no response left to modify. A ",[603,832,833],{},"StreamingResponse"," that has already emitted its first chunk has committed a status code, so raising afterwards cannot change it. And a middleware that wraps ",[603,836,837],{},"call_next"," in a bare ",[603,840,841],{},"except"," swallows the exception before the exception middleware sees it — ",[633,844,846],{"href":845},"\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-execution-order\u002F","Middleware Execution Order"," covers where in the stack that goes wrong.",[639,849,851],{"id":850},"the-fix","The Fix",[590,853,854,855,857],{},"Split the two responsibilities. The service raises domain exceptions carrying a code, a message and structured context. One handler maps that hierarchy onto the wire format. ",[603,856,605],{}," stays in the path operation, where the failure genuinely is about HTTP.",[859,860,865],"pre",{"className":861,"code":862,"language":863,"meta":864,"style":864},"language-python shiki shiki-themes github-light-high-contrast","\"\"\"Contrast raising HTTPException in a route with mapping a domain exception via a handler.\"\"\"\nfrom fastapi import FastAPI, HTTPException, Request\nfrom fastapi.responses import JSONResponse\n\n\nclass DomainError(Exception):\n    \"\"\"Base for everything the domain can refuse to do.\"\"\"\n\n    code = \"domain_error\"\n    status = 400\n\n    def __init__(self, message: str, **context: object) -> None:\n        super().__init__(message)\n        self.message = message\n        self.context = context\n\n\nclass AccountNotFound(DomainError):\n    code = \"account_not_found\"\n    status = 404\n\n\nclass InsufficientFunds(DomainError):\n    code = \"insufficient_funds\"\n    status = 409\n\n\nBALANCES = {\"acc_1\": 500}\n\n\ndef withdraw(account_id: str, amount: int) -> int:\n    \"\"\"Pure domain logic. Importable from a worker, a CLI or a test with no HTTP in sight.\"\"\"\n    if account_id not in BALANCES:\n        raise AccountNotFound(\"no such account\", account_id=account_id)\n    if BALANCES[account_id] \u003C amount:\n        raise InsufficientFunds(\n            \"balance too low\", account_id=account_id, balance=BALANCES[account_id], requested=amount\n        )\n    BALANCES[account_id] -= amount\n    return BALANCES[account_id]\n","python","",[603,866,867,875,892,905,912,917,937,943,948,960,971,976,1011,1026,1040,1053,1058,1063,1078,1088,1098,1103,1108,1122,1132,1142,1147,1152,1176,1181,1186,1213,1219,1239,1261,1277,1285,1318,1324,1338],{"__ignoreMap":864},[868,869,871],"span",{"class":714,"line":870},1,[868,872,874],{"class":873},"sYEJz","\"\"\"Contrast raising HTTPException in a route with mapping a domain exception via a handler.\"\"\"\n",[868,876,878,882,886,889],{"class":714,"line":877},2,[868,879,881],{"class":880},"sTJeM","from",[868,883,885],{"class":884},"sigWx"," fastapi ",[868,887,888],{"class":880},"import",[868,890,891],{"class":884}," FastAPI, HTTPException, Request\n",[868,893,895,897,900,902],{"class":714,"line":894},3,[868,896,881],{"class":880},[868,898,899],{"class":884}," fastapi.responses ",[868,901,888],{"class":880},[868,903,904],{"class":884}," JSONResponse\n",[868,906,908],{"class":714,"line":907},4,[868,909,911],{"emptyLinePlaceholder":910},true,"\n",[868,913,915],{"class":714,"line":914},5,[868,916,911],{"emptyLinePlaceholder":910},[868,918,920,923,927,930,934],{"class":714,"line":919},6,[868,921,922],{"class":880},"class",[868,924,926],{"class":925},"sV4o_"," DomainError",[868,928,929],{"class":884},"(",[868,931,933],{"class":932},"sacAq","Exception",[868,935,936],{"class":884},"):\n",[868,938,940],{"class":714,"line":939},7,[868,941,942],{"class":873},"    \"\"\"Base for everything the domain can refuse to do.\"\"\"\n",[868,944,946],{"class":714,"line":945},8,[868,947,911],{"emptyLinePlaceholder":910},[868,949,951,954,957],{"class":714,"line":950},9,[868,952,953],{"class":884},"    code ",[868,955,956],{"class":880},"=",[868,958,959],{"class":873}," \"domain_error\"\n",[868,961,963,966,968],{"class":714,"line":962},10,[868,964,965],{"class":884},"    status ",[868,967,956],{"class":880},[868,969,970],{"class":932}," 400\n",[868,972,974],{"class":714,"line":973},11,[868,975,911],{"emptyLinePlaceholder":910},[868,977,979,982,985,988,991,993,996,999,1002,1005,1008],{"class":714,"line":978},12,[868,980,981],{"class":880},"    def",[868,983,984],{"class":932}," __init__",[868,986,987],{"class":884},"(self, message: ",[868,989,990],{"class":932},"str",[868,992,819],{"class":884},[868,994,995],{"class":880},"**",[868,997,998],{"class":884},"context: ",[868,1000,1001],{"class":932},"object",[868,1003,1004],{"class":884},") -> ",[868,1006,1007],{"class":932},"None",[868,1009,1010],{"class":884},":\n",[868,1012,1014,1017,1020,1023],{"class":714,"line":1013},13,[868,1015,1016],{"class":932},"        super",[868,1018,1019],{"class":884},"().",[868,1021,1022],{"class":932},"__init__",[868,1024,1025],{"class":884},"(message)\n",[868,1027,1029,1032,1035,1037],{"class":714,"line":1028},14,[868,1030,1031],{"class":932},"        self",[868,1033,1034],{"class":884},".message ",[868,1036,956],{"class":880},[868,1038,1039],{"class":884}," message\n",[868,1041,1043,1045,1048,1050],{"class":714,"line":1042},15,[868,1044,1031],{"class":932},[868,1046,1047],{"class":884},".context ",[868,1049,956],{"class":880},[868,1051,1052],{"class":884}," context\n",[868,1054,1056],{"class":714,"line":1055},16,[868,1057,911],{"emptyLinePlaceholder":910},[868,1059,1061],{"class":714,"line":1060},17,[868,1062,911],{"emptyLinePlaceholder":910},[868,1064,1066,1068,1071,1073,1076],{"class":714,"line":1065},18,[868,1067,922],{"class":880},[868,1069,1070],{"class":925}," AccountNotFound",[868,1072,929],{"class":884},[868,1074,1075],{"class":932},"DomainError",[868,1077,936],{"class":884},[868,1079,1081,1083,1085],{"class":714,"line":1080},19,[868,1082,953],{"class":884},[868,1084,956],{"class":880},[868,1086,1087],{"class":873}," \"account_not_found\"\n",[868,1089,1091,1093,1095],{"class":714,"line":1090},20,[868,1092,965],{"class":884},[868,1094,956],{"class":880},[868,1096,1097],{"class":932}," 404\n",[868,1099,1101],{"class":714,"line":1100},21,[868,1102,911],{"emptyLinePlaceholder":910},[868,1104,1106],{"class":714,"line":1105},22,[868,1107,911],{"emptyLinePlaceholder":910},[868,1109,1111,1113,1116,1118,1120],{"class":714,"line":1110},23,[868,1112,922],{"class":880},[868,1114,1115],{"class":925}," InsufficientFunds",[868,1117,929],{"class":884},[868,1119,1075],{"class":932},[868,1121,936],{"class":884},[868,1123,1125,1127,1129],{"class":714,"line":1124},24,[868,1126,953],{"class":884},[868,1128,956],{"class":880},[868,1130,1131],{"class":873}," \"insufficient_funds\"\n",[868,1133,1135,1137,1139],{"class":714,"line":1134},25,[868,1136,965],{"class":884},[868,1138,956],{"class":880},[868,1140,1141],{"class":932}," 409\n",[868,1143,1145],{"class":714,"line":1144},26,[868,1146,911],{"emptyLinePlaceholder":910},[868,1148,1150],{"class":714,"line":1149},27,[868,1151,911],{"emptyLinePlaceholder":910},[868,1153,1155,1158,1161,1164,1167,1170,1173],{"class":714,"line":1154},28,[868,1156,1157],{"class":932},"BALANCES",[868,1159,1160],{"class":880}," =",[868,1162,1163],{"class":884}," {",[868,1165,1166],{"class":873},"\"acc_1\"",[868,1168,1169],{"class":884},": ",[868,1171,1172],{"class":932},"500",[868,1174,1175],{"class":884},"}\n",[868,1177,1179],{"class":714,"line":1178},29,[868,1180,911],{"emptyLinePlaceholder":910},[868,1182,1184],{"class":714,"line":1183},30,[868,1185,911],{"emptyLinePlaceholder":910},[868,1187,1189,1192,1196,1199,1201,1204,1207,1209,1211],{"class":714,"line":1188},31,[868,1190,1191],{"class":880},"def",[868,1193,1195],{"class":1194},"s3dhs"," withdraw",[868,1197,1198],{"class":884},"(account_id: ",[868,1200,990],{"class":932},[868,1202,1203],{"class":884},", amount: ",[868,1205,1206],{"class":932},"int",[868,1208,1004],{"class":884},[868,1210,1206],{"class":932},[868,1212,1010],{"class":884},[868,1214,1216],{"class":714,"line":1215},32,[868,1217,1218],{"class":873},"    \"\"\"Pure domain logic. Importable from a worker, a CLI or a test with no HTTP in sight.\"\"\"\n",[868,1220,1222,1225,1228,1231,1234,1237],{"class":714,"line":1221},33,[868,1223,1224],{"class":880},"    if",[868,1226,1227],{"class":884}," account_id ",[868,1229,1230],{"class":880},"not",[868,1232,1233],{"class":880}," in",[868,1235,1236],{"class":932}," BALANCES",[868,1238,1010],{"class":884},[868,1240,1242,1245,1248,1251,1253,1256,1258],{"class":714,"line":1241},34,[868,1243,1244],{"class":880},"        raise",[868,1246,1247],{"class":884}," AccountNotFound(",[868,1249,1250],{"class":873},"\"no such account\"",[868,1252,819],{"class":884},[868,1254,1255],{"class":925},"account_id",[868,1257,956],{"class":880},[868,1259,1260],{"class":884},"account_id)\n",[868,1262,1264,1266,1268,1271,1274],{"class":714,"line":1263},35,[868,1265,1224],{"class":880},[868,1267,1236],{"class":932},[868,1269,1270],{"class":884},"[account_id] ",[868,1272,1273],{"class":880},"\u003C",[868,1275,1276],{"class":884}," amount:\n",[868,1278,1280,1282],{"class":714,"line":1279},36,[868,1281,1244],{"class":880},[868,1283,1284],{"class":884}," InsufficientFunds(\n",[868,1286,1288,1291,1293,1295,1297,1300,1303,1305,1307,1310,1313,1315],{"class":714,"line":1287},37,[868,1289,1290],{"class":873},"            \"balance too low\"",[868,1292,819],{"class":884},[868,1294,1255],{"class":925},[868,1296,956],{"class":880},[868,1298,1299],{"class":884},"account_id, ",[868,1301,1302],{"class":925},"balance",[868,1304,956],{"class":880},[868,1306,1157],{"class":932},[868,1308,1309],{"class":884},"[account_id], ",[868,1311,1312],{"class":925},"requested",[868,1314,956],{"class":880},[868,1316,1317],{"class":884},"amount\n",[868,1319,1321],{"class":714,"line":1320},38,[868,1322,1323],{"class":884},"        )\n",[868,1325,1327,1330,1332,1335],{"class":714,"line":1326},39,[868,1328,1329],{"class":932},"    BALANCES",[868,1331,1270],{"class":884},[868,1333,1334],{"class":880},"-=",[868,1336,1337],{"class":884}," amount\n",[868,1339,1341,1344,1346],{"class":714,"line":1340},40,[868,1342,1343],{"class":880},"    return",[868,1345,1236],{"class":932},[868,1347,1348],{"class":884},"[account_id]\n",[590,1350,1351,1352,1355,1356,1359],{},"Note what ",[603,1353,1354],{},"withdraw"," does not import. No FastAPI, no status codes, no response classes. It is testable with a plain ",[603,1357,1358],{},"pytest.raises"," and callable from anywhere.",[590,1361,1362],{},"The HTTP layer is then thin, and there is exactly one place that decides what a domain failure looks like on the wire:",[859,1364,1366],{"className":861,"code":1365,"language":863,"meta":864,"style":864},"app = FastAPI()\n\n\n@app.exception_handler(DomainError)\nasync def domain_error_handler(request: Request, exc: DomainError) -> JSONResponse:\n    \"\"\"One place that decides how every domain failure looks on the wire.\"\"\"\n    return JSONResponse(\n        status_code=exc.status,\n        content={\n            \"error\": {\"code\": exc.code, \"message\": exc.message, \"context\": exc.context},\n            \"path\": request.url.path,\n        },\n    )\n\n\n@app.post(\"\u002Faccounts\u002F{account_id}\u002Fwithdrawals\")\nasync def make_withdrawal(account_id: str, amount: int) -> dict[str, int | str]:\n    \"\"\"No try\u002Fexcept. The service raises; the handler renders.\"\"\"\n    remaining = withdraw(account_id, amount)\n    return {\"account_id\": account_id, \"remaining_cents\": remaining}\n\n\n@app.get(\"\u002Faccounts\u002F{account_id}\")\nasync def read_account(account_id: str) -> dict[str, int | str]:\n    \"\"\"HTTPException is right here: this IS the transport concern, raised at the edge.\"\"\"\n    if account_id not in BALANCES:\n        raise HTTPException(\n            status_code=404,\n            detail=\"Account not found\",\n            headers={\"Cache-Control\": \"no-store\"},\n        )\n    return {\"account_id\": account_id, \"balance_cents\": BALANCES[account_id]}\n",[603,1367,1368,1378,1382,1386,1394,1408,1413,1420,1430,1440,1466,1474,1479,1484,1488,1492,1511,1546,1551,1561,1579,1583,1587,1603,1630,1635,1649,1656,1669,1681,1702,1706],{"__ignoreMap":864},[868,1369,1370,1373,1375],{"class":714,"line":870},[868,1371,1372],{"class":884},"app ",[868,1374,956],{"class":880},[868,1376,1377],{"class":884}," FastAPI()\n",[868,1379,1380],{"class":714,"line":877},[868,1381,911],{"emptyLinePlaceholder":910},[868,1383,1384],{"class":714,"line":894},[868,1385,911],{"emptyLinePlaceholder":910},[868,1387,1388,1391],{"class":714,"line":907},[868,1389,1390],{"class":1194},"@app.exception_handler",[868,1392,1393],{"class":884},"(DomainError)\n",[868,1395,1396,1399,1402,1405],{"class":714,"line":914},[868,1397,1398],{"class":880},"async",[868,1400,1401],{"class":880}," def",[868,1403,1404],{"class":1194}," domain_error_handler",[868,1406,1407],{"class":884},"(request: Request, exc: DomainError) -> JSONResponse:\n",[868,1409,1410],{"class":714,"line":919},[868,1411,1412],{"class":873},"    \"\"\"One place that decides how every domain failure looks on the wire.\"\"\"\n",[868,1414,1415,1417],{"class":714,"line":939},[868,1416,1343],{"class":880},[868,1418,1419],{"class":884}," JSONResponse(\n",[868,1421,1422,1425,1427],{"class":714,"line":945},[868,1423,1424],{"class":925},"        status_code",[868,1426,956],{"class":880},[868,1428,1429],{"class":884},"exc.status,\n",[868,1431,1432,1435,1437],{"class":714,"line":950},[868,1433,1434],{"class":925},"        content",[868,1436,956],{"class":880},[868,1438,1439],{"class":884},"{\n",[868,1441,1442,1445,1448,1451,1454,1457,1460,1463],{"class":714,"line":962},[868,1443,1444],{"class":873},"            \"error\"",[868,1446,1447],{"class":884},": {",[868,1449,1450],{"class":873},"\"code\"",[868,1452,1453],{"class":884},": exc.code, ",[868,1455,1456],{"class":873},"\"message\"",[868,1458,1459],{"class":884},": exc.message, ",[868,1461,1462],{"class":873},"\"context\"",[868,1464,1465],{"class":884},": exc.context},\n",[868,1467,1468,1471],{"class":714,"line":973},[868,1469,1470],{"class":873},"            \"path\"",[868,1472,1473],{"class":884},": request.url.path,\n",[868,1475,1476],{"class":714,"line":978},[868,1477,1478],{"class":884},"        },\n",[868,1480,1481],{"class":714,"line":1013},[868,1482,1483],{"class":884},"    )\n",[868,1485,1486],{"class":714,"line":1028},[868,1487,911],{"emptyLinePlaceholder":910},[868,1489,1490],{"class":714,"line":1042},[868,1491,911],{"emptyLinePlaceholder":910},[868,1493,1494,1497,1499,1502,1505,1508],{"class":714,"line":1055},[868,1495,1496],{"class":1194},"@app.post",[868,1498,929],{"class":884},[868,1500,1501],{"class":873},"\"\u002Faccounts\u002F",[868,1503,1504],{"class":880},"{account_id}",[868,1506,1507],{"class":873},"\u002Fwithdrawals\"",[868,1509,1510],{"class":884},")\n",[868,1512,1513,1515,1517,1520,1522,1524,1526,1528,1531,1533,1535,1537,1540,1543],{"class":714,"line":1060},[868,1514,1398],{"class":880},[868,1516,1401],{"class":880},[868,1518,1519],{"class":1194}," make_withdrawal",[868,1521,1198],{"class":884},[868,1523,990],{"class":932},[868,1525,1203],{"class":884},[868,1527,1206],{"class":932},[868,1529,1530],{"class":884},") -> dict[",[868,1532,990],{"class":932},[868,1534,819],{"class":884},[868,1536,1206],{"class":932},[868,1538,1539],{"class":880}," |",[868,1541,1542],{"class":932}," str",[868,1544,1545],{"class":884},"]:\n",[868,1547,1548],{"class":714,"line":1065},[868,1549,1550],{"class":873},"    \"\"\"No try\u002Fexcept. The service raises; the handler renders.\"\"\"\n",[868,1552,1553,1556,1558],{"class":714,"line":1080},[868,1554,1555],{"class":884},"    remaining ",[868,1557,956],{"class":880},[868,1559,1560],{"class":884}," withdraw(account_id, amount)\n",[868,1562,1563,1565,1567,1570,1573,1576],{"class":714,"line":1090},[868,1564,1343],{"class":880},[868,1566,1163],{"class":884},[868,1568,1569],{"class":873},"\"account_id\"",[868,1571,1572],{"class":884},": account_id, ",[868,1574,1575],{"class":873},"\"remaining_cents\"",[868,1577,1578],{"class":884},": remaining}\n",[868,1580,1581],{"class":714,"line":1100},[868,1582,911],{"emptyLinePlaceholder":910},[868,1584,1585],{"class":714,"line":1105},[868,1586,911],{"emptyLinePlaceholder":910},[868,1588,1589,1592,1594,1596,1598,1601],{"class":714,"line":1110},[868,1590,1591],{"class":1194},"@app.get",[868,1593,929],{"class":884},[868,1595,1501],{"class":873},[868,1597,1504],{"class":880},[868,1599,1600],{"class":873},"\"",[868,1602,1510],{"class":884},[868,1604,1605,1607,1609,1612,1614,1616,1618,1620,1622,1624,1626,1628],{"class":714,"line":1124},[868,1606,1398],{"class":880},[868,1608,1401],{"class":880},[868,1610,1611],{"class":1194}," read_account",[868,1613,1198],{"class":884},[868,1615,990],{"class":932},[868,1617,1530],{"class":884},[868,1619,990],{"class":932},[868,1621,819],{"class":884},[868,1623,1206],{"class":932},[868,1625,1539],{"class":880},[868,1627,1542],{"class":932},[868,1629,1545],{"class":884},[868,1631,1632],{"class":714,"line":1134},[868,1633,1634],{"class":873},"    \"\"\"HTTPException is right here: this IS the transport concern, raised at the edge.\"\"\"\n",[868,1636,1637,1639,1641,1643,1645,1647],{"class":714,"line":1144},[868,1638,1224],{"class":880},[868,1640,1227],{"class":884},[868,1642,1230],{"class":880},[868,1644,1233],{"class":880},[868,1646,1236],{"class":932},[868,1648,1010],{"class":884},[868,1650,1651,1653],{"class":714,"line":1149},[868,1652,1244],{"class":880},[868,1654,1655],{"class":884}," HTTPException(\n",[868,1657,1658,1661,1663,1666],{"class":714,"line":1154},[868,1659,1660],{"class":925},"            status_code",[868,1662,956],{"class":880},[868,1664,1665],{"class":932},"404",[868,1667,1668],{"class":884},",\n",[868,1670,1671,1674,1676,1679],{"class":714,"line":1178},[868,1672,1673],{"class":925},"            detail",[868,1675,956],{"class":880},[868,1677,1678],{"class":873},"\"Account not found\"",[868,1680,1668],{"class":884},[868,1682,1683,1686,1688,1691,1694,1696,1699],{"class":714,"line":1183},[868,1684,1685],{"class":925},"            headers",[868,1687,956],{"class":880},[868,1689,1690],{"class":884},"{",[868,1692,1693],{"class":873},"\"Cache-Control\"",[868,1695,1169],{"class":884},[868,1697,1698],{"class":873},"\"no-store\"",[868,1700,1701],{"class":884},"},\n",[868,1703,1704],{"class":714,"line":1188},[868,1705,1323],{"class":884},[868,1707,1708,1710,1712,1714,1716,1719,1721,1723],{"class":714,"line":1215},[868,1709,1343],{"class":880},[868,1711,1163],{"class":884},[868,1713,1569],{"class":873},[868,1715,1572],{"class":884},[868,1717,1718],{"class":873},"\"balance_cents\"",[868,1720,1169],{"class":884},[868,1722,1157],{"class":932},[868,1724,1725],{"class":884},"[account_id]}\n",[590,1727,1728],{},"Both styles, running in the same app, from a real run:",[859,1730,1734],{"className":1731,"code":1733,"language":670,"meta":864},[1732],"language-text","$ POST \u002Faccounts\u002Facc_1\u002Fwithdrawals?amount=100\n200 OK\n{\n  \"account_id\": \"acc_1\",\n  \"remaining_cents\": 400\n}\n\n$ POST \u002Faccounts\u002Facc_1\u002Fwithdrawals?amount=100000\n409 Conflict\n{\n  \"error\": {\n    \"code\": \"insufficient_funds\",\n    \"message\": \"balance too low\",\n    \"context\": {\n      \"account_id\": \"acc_1\",\n      \"balance\": 400,\n      \"requested\": 100000\n    }\n  },\n  \"path\": \"\u002Faccounts\u002Facc_1\u002Fwithdrawals\"\n}\n\n$ POST \u002Faccounts\u002Facc_nope\u002Fwithdrawals?amount=1\n404 Not Found\n{\n  \"error\": {\n    \"code\": \"account_not_found\",\n    \"message\": \"no such account\",\n    \"context\": {\n      \"account_id\": \"acc_nope\"\n    }\n  },\n  \"path\": \"\u002Faccounts\u002Facc_nope\u002Fwithdrawals\"\n}\n\n$ GET \u002Faccounts\u002Facc_nope\n404 Not Found\n{\n  \"detail\": \"Account not found\"\n}\n",[603,1735,1733],{"__ignoreMap":864},[590,1737,1738],{},"Three details in that transcript are the argument for the pattern.",[590,1740,1741,1742,1746,1747,1749,1750,1752,1753,1755,1756,1759,1760,1763],{},"The 409 and the first 404 came from ",[1743,1744,1745],"em",{},"one"," registered handler. Neither ",[603,1748,818],{}," nor ",[603,1751,822],{}," has a handler of its own; the MRO walk found the ",[603,1754,1075],{}," registration. Adding a ",[603,1757,1758],{},"CardDeclined"," with ",[603,1761,1762],{},"status = 402"," requires no change to the HTTP layer at all.",[590,1765,1766,1767,1770,1771,1774,1775,1777],{},"The status code came from the exception class, not from the route. ",[603,1768,1769],{},"make_withdrawal"," contains no status codes and no ",[603,1772,1773],{},"try","\u002F",[603,1776,841],{},", so the mapping from business failure to HTTP semantics lives in one readable place rather than scattered across handlers.",[590,1779,1780,1781,1784,1785,1788],{},"And the ",[603,1782,1783],{},"context"," dictionary carries the actual numbers — balance 400, requested 100000 — which a client can render into a real message. ",[603,1786,1787],{},"{\"detail\": \"Insufficient funds\"}"," cannot. Note the balance reads 400 and not 500, because the successful withdrawal in the first request genuinely mutated the state before the second request ran.",[590,1790,1791,1792,1794,1795,1797],{},"The last response shows the contrasting shape. ",[603,1793,605],{}," produces Starlette's ",[603,1796,803],{}," envelope, and it is the right tool there: \"you asked for an account that does not exist in this URL\" is a statement about the request, not about the business rule.",[639,1799,1801],{"id":1800},"verification","Verification",[590,1803,1804],{},"Test the two layers separately, which is the payoff of having separated them:",[859,1806,1808],{"className":861,"code":1807,"language":863,"meta":864,"style":864},"def test_service_layer_has_no_http_concerns():\n    with pytest.raises(InsufficientFunds) as excinfo:\n        withdraw(\"acc_1\", 10**9)\n    assert excinfo.value.context[\"requested\"] == 10**9\n\n\ndef test_handler_maps_status_and_code(client):\n    response = client.post(\"\u002Faccounts\u002Facc_1\u002Fwithdrawals\", params={\"amount\": 10**9})\n    assert response.status_code == 409\n    assert response.json()[\"error\"][\"code\"] == \"insufficient_funds\"\n",[603,1809,1810,1820,1834,1853,1878,1882,1886,1896,1932,1943],{"__ignoreMap":864},[868,1811,1812,1814,1817],{"class":714,"line":870},[868,1813,1191],{"class":880},[868,1815,1816],{"class":1194}," test_service_layer_has_no_http_concerns",[868,1818,1819],{"class":884},"():\n",[868,1821,1822,1825,1828,1831],{"class":714,"line":877},[868,1823,1824],{"class":880},"    with",[868,1826,1827],{"class":884}," pytest.raises(InsufficientFunds) ",[868,1829,1830],{"class":880},"as",[868,1832,1833],{"class":884}," excinfo:\n",[868,1835,1836,1839,1841,1843,1846,1848,1851],{"class":714,"line":894},[868,1837,1838],{"class":884},"        withdraw(",[868,1840,1166],{"class":873},[868,1842,819],{"class":884},[868,1844,1845],{"class":932},"10",[868,1847,995],{"class":880},[868,1849,1850],{"class":932},"9",[868,1852,1510],{"class":884},[868,1854,1855,1858,1861,1864,1867,1870,1873,1875],{"class":714,"line":907},[868,1856,1857],{"class":880},"    assert",[868,1859,1860],{"class":884}," excinfo.value.context[",[868,1862,1863],{"class":873},"\"requested\"",[868,1865,1866],{"class":884},"] ",[868,1868,1869],{"class":880},"==",[868,1871,1872],{"class":932}," 10",[868,1874,995],{"class":880},[868,1876,1877],{"class":932},"9\n",[868,1879,1880],{"class":714,"line":914},[868,1881,911],{"emptyLinePlaceholder":910},[868,1883,1884],{"class":714,"line":919},[868,1885,911],{"emptyLinePlaceholder":910},[868,1887,1888,1890,1893],{"class":714,"line":939},[868,1889,1191],{"class":880},[868,1891,1892],{"class":1194}," test_handler_maps_status_and_code",[868,1894,1895],{"class":884},"(client):\n",[868,1897,1898,1901,1903,1906,1909,1911,1914,1916,1918,1921,1923,1925,1927,1929],{"class":714,"line":945},[868,1899,1900],{"class":884},"    response ",[868,1902,956],{"class":880},[868,1904,1905],{"class":884}," client.post(",[868,1907,1908],{"class":873},"\"\u002Faccounts\u002Facc_1\u002Fwithdrawals\"",[868,1910,819],{"class":884},[868,1912,1913],{"class":925},"params",[868,1915,956],{"class":880},[868,1917,1690],{"class":884},[868,1919,1920],{"class":873},"\"amount\"",[868,1922,1169],{"class":884},[868,1924,1845],{"class":932},[868,1926,995],{"class":880},[868,1928,1850],{"class":932},[868,1930,1931],{"class":884},"})\n",[868,1933,1934,1936,1939,1941],{"class":714,"line":950},[868,1935,1857],{"class":880},[868,1937,1938],{"class":884}," response.status_code ",[868,1940,1869],{"class":880},[868,1942,1141],{"class":932},[868,1944,1945,1947,1950,1953,1956,1958,1960,1962],{"class":714,"line":962},[868,1946,1857],{"class":880},[868,1948,1949],{"class":884}," response.json()[",[868,1951,1952],{"class":873},"\"error\"",[868,1954,1955],{"class":884},"][",[868,1957,1450],{"class":873},[868,1959,1866],{"class":884},[868,1961,1869],{"class":880},[868,1963,1131],{"class":873},[590,1965,1966],{},"Then add the guard that keeps the boundary from eroding, because it will erode under deadline pressure:",[859,1968,1970],{"className":861,"code":1969,"language":863,"meta":864,"style":864},"def test_service_package_never_imports_fastapi():\n    source = (Path(\"app\") \u002F \"services\").rglob(\"*.py\")\n    for path in source:\n        assert \"fastapi\" not in path.read_text(), f\"{path} imports the web framework\"\n",[603,1971,1972,1981,2010,2024],{"__ignoreMap":864},[868,1973,1974,1976,1979],{"class":714,"line":870},[868,1975,1191],{"class":880},[868,1977,1978],{"class":1194}," test_service_package_never_imports_fastapi",[868,1980,1819],{"class":884},[868,1982,1983,1986,1988,1991,1994,1997,1999,2002,2005,2008],{"class":714,"line":877},[868,1984,1985],{"class":884},"    source ",[868,1987,956],{"class":880},[868,1989,1990],{"class":884}," (Path(",[868,1992,1993],{"class":873},"\"app\"",[868,1995,1996],{"class":884},") ",[868,1998,1774],{"class":880},[868,2000,2001],{"class":873}," \"services\"",[868,2003,2004],{"class":884},").rglob(",[868,2006,2007],{"class":873},"\"*.py\"",[868,2009,1510],{"class":884},[868,2011,2012,2015,2018,2021],{"class":714,"line":894},[868,2013,2014],{"class":880},"    for",[868,2016,2017],{"class":884}," path ",[868,2019,2020],{"class":880},"in",[868,2022,2023],{"class":884}," source:\n",[868,2025,2026,2029,2032,2035,2037,2040,2043,2045,2047,2050,2053],{"class":714,"line":907},[868,2027,2028],{"class":880},"        assert",[868,2030,2031],{"class":873}," \"fastapi\"",[868,2033,2034],{"class":880}," not",[868,2036,1233],{"class":880},[868,2038,2039],{"class":884}," path.read_text(), ",[868,2041,2042],{"class":880},"f",[868,2044,1600],{"class":873},[868,2046,1690],{"class":880},[868,2048,2049],{"class":884},"path",[868,2051,2052],{"class":880},"}",[868,2054,2055],{"class":873}," imports the web framework\"\n",[590,2057,2058,2059,2061],{},"That test is blunt and it is effective. It fails on the first pull request that puts an ",[603,2060,605],{}," back into a service, at review time rather than at the point somebody tries to reuse the module.",[590,2063,2064,2065,2068,2069,2072,2073,2076],{},"In production, alert on the ",[603,2066,2067],{},"error.code"," field rather than on the status code. A spike in ",[603,2070,2071],{},"insufficient_funds"," is a product signal; a spike in ",[603,2074,2075],{},"account_not_found"," is usually a caller bug or an enumeration attempt. Both are 4xx and only the code tells them apart.",[639,2078,2080],{"id":2079},"trade-offs-and-when-not-to","Trade-offs and When Not To",[590,2082,2083,2086,2087,2089],{},[593,2084,2085],{},"A hierarchy is overhead on a small API."," Three endpoints and two error cases do not need a base class and a handler. Raise ",[603,2088,605],{}," and move on. The pattern pays for itself when the same business logic has more than one caller, or when the number of error types passes roughly a dozen.",[590,2091,2092,2097,2098,2101,2102,2104],{},[593,2093,2094,2096],{},[603,2095,605],{}," is a genuinely good tool at the edge."," Authentication failures, unsupported media types, a ",[603,2099,2100],{},"Retry-After"," on a throttle — these are HTTP facts, and expressing them as domain exceptions is the mirror-image mistake. The rule is not \"never raise ",[603,2103,605],{},"\", it is \"never raise it below the layer that knows it is serving HTTP\".",[590,2106,2107,2110,2111,2113,2114,2117],{},[593,2108,2109],{},"Domain exceptions do not document themselves."," ",[603,2112,605],{}," in a route tells a reader nothing about OpenAPI either, but at least the status code is visible. With mapped exceptions the possible status codes of an endpoint are invisible unless you declare them, so add ",[603,2115,2116],{},"responses={409: {...}}"," to the decorator for anything a client must handle.",[590,2119,2120,2123,2124,2126],{},[593,2121,2122],{},"Do not put both mechanisms on the same failure."," Catching a domain exception in a route and re-raising it as an ",[603,2125,605],{}," reintroduces the coupling one layer up and gives you two places to change a status code. If a specific route genuinely needs a different status than the class default, that is a signal the class is too coarse.",[590,2128,2129,2132,2133,2136,2137,2141],{},[593,2130,2131],{},"Validation errors are a separate track."," Pydantic's ",[603,2134,2135],{},"RequestValidationError"," has its own handler and its own body shape, and reshaping it is covered in ",[633,2138,2140],{"href":2139},"\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fcustomising-validation-error-responses\u002F","Customising Validation Error Responses",". Do not try to fold 422s into a domain hierarchy — they are raised before your code runs.",[590,2143,2144,2145,2148],{},"For the surrounding envelope, logging and correlation-id conventions, see ",[633,2146,485],{"href":2147},"\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fglobal-exception-handlers-for-consistent-api-responses\u002F",".",[639,2150,2152],{"id":2151},"faq","FAQ",[590,2154,2155,2158,2159,2161],{},[593,2156,2157],{},"When should I raise HTTPException instead of a domain exception?","\nRaise ",[603,2160,605],{}," at the edge, where the failure genuinely is an HTTP concern — a missing path parameter, a failed auth check, a request header you cannot honour. Raise a domain exception whenever the failure is about the business rule, so the code expressing that rule stays callable from a worker or a CLI.",[590,2163,2164,2167,2168,2170],{},[593,2165,2166],{},"Does one exception handler catch subclasses of the exception it registers?","\nYes. Starlette looks up handlers by walking the raised exception's method resolution order and using the first registered match, so a handler registered for a ",[603,2169,1075],{}," base class catches every subclass. One registration covers a whole hierarchy.",[590,2172,2173,2176,2177,2179,2180,1774,2182,2184],{},[593,2174,2175],{},"Why does my exception handler not fire?","\nThe three usual causes are raising inside a ",[603,2178,829],{},", which runs after the response has been sent, raising after a streaming response has begun, and catching the exception yourself in a broad ",[603,2181,1773],{},[603,2183,841],{}," in the route. A middleware that swallows exceptions will also shadow it.",[590,2186,2187,2190,2191,612,2193,2195],{},[593,2188,2189],{},"How do I keep the error response shape consistent across an API?","\nGive every domain exception a ",[603,2192,603],{},[603,2194,615],{}," on the class, register one handler for the base class, and let that single function build the JSON envelope. Every new exception type then inherits the format for free rather than reimplementing it.",[590,2197,2198,2201,2203,2204,2206,2207,2209,2210,2212,2213,2216],{},[593,2199,2200],{},"Can I attach response headers from an exception?",[603,2202,605],{}," takes a ",[603,2205,807],{}," argument, and a custom handler returns a full ",[603,2208,799],{}," so it can set anything — ",[603,2211,2100],{}," on a rate limit, ",[603,2214,2215],{},"Cache-Control: no-store"," on a 404 that must not be cached, or a correlation id.",[639,2218,2220],{"id":2219},"related-reading","Related Reading",[597,2222,2223,2230,2235,2240,2245],{},[600,2224,2225,2110,2228,2148],{},[593,2226,2227],{},"Up to the topic:",[633,2229,636],{"href":635},[600,2231,2232,2233,2148],{},"The response envelope and logging conventions: ",[633,2234,485],{"href":2147},[600,2236,2237,2238,2148],{},"The 422 track, which never reaches your handlers: ",[633,2239,2140],{"href":2139},[600,2241,2242,2243,2148],{},"Why a swallowing middleware hides your handler: ",[633,2244,846],{"href":845},[600,2246,2247,2248,2148],{},"Where the service objects that raise these exceptions come from: ",[633,2249,2251],{"href":2250},"\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002F","Dependency Injection Strategies",[2253,2254,2255],"style",{},"html pre.shiki code .sYEJz, html code.shiki .sYEJz{--shiki-default:#032563}html pre.shiki code .sTJeM, html code.shiki .sTJeM{--shiki-default:#A0111F}html pre.shiki code .sigWx, html code.shiki .sigWx{--shiki-default:#0E1116}html pre.shiki code .sV4o_, html code.shiki .sV4o_{--shiki-default:#702C00}html pre.shiki code .sacAq, html code.shiki .sacAq{--shiki-default:#023B95}html pre.shiki code .s3dhs, html code.shiki .s3dhs{--shiki-default:#622CBC}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":864,"searchDepth":877,"depth":877,"links":2257},[2258,2259,2260,2261,2262,2263,2264],{"id":641,"depth":877,"text":642},{"id":775,"depth":877,"text":776},{"id":850,"depth":877,"text":851},{"id":1800,"depth":877,"text":1801},{"id":2079,"depth":877,"text":2080},{"id":2151,"depth":877,"text":2152},{"id":2219,"depth":877,"text":2220},"2026-07-20","When to raise HTTPException versus a domain exception mapped by a handler in FastAPI, and how to keep HTTP status codes out of your service layer entirely.","md",[2269,2271,2273,2275,2277],{"q":2157,"a":2270},"Raise HTTPException at the edge, where the failure genuinely is an HTTP concern — a missing path parameter, a failed auth check, a request header you cannot honour. Raise a domain exception whenever the failure is about the business rule, so the code expressing that rule stays callable from a worker or a CLI.",{"q":2166,"a":2272},"Yes. Starlette looks up handlers by walking the raised exception's method resolution order and using the first registered match, so a handler registered for a DomainError base class catches every subclass. One registration covers a whole hierarchy.",{"q":2175,"a":2274},"The three usual causes are raising inside a BackgroundTask, which runs after the response has been sent, raising after a streaming response has begun, and catching the exception yourself in a broad try\u002Fexcept in the route. A middleware that swallows exceptions will also shadow it.",{"q":2189,"a":2276},"Give every domain exception a code and a status on the class, register one handler for the base class, and let that single function build the JSON envelope. Every new exception type then inherits the format for free rather than reimplementing it.",{"q":2200,"a":2278},"HTTPException takes a headers argument, and a custom handler returns a full JSONResponse so it can set anything — Retry-After on a rate limit, Cache-Control: no-store on a 404 that must not be cached, or a correlation id.",null,{"slug":2281,"breadcrumb":2282},"httpexception-vs-custom-exception-classes",[2283,2285,2288,2290],{"label":2284,"path":1774},"Home",{"label":2286,"path":2287},"Core Architecture & Routing Patterns","\u002Fcore-architecture-routing-patterns\u002F",{"label":2289,"path":635},"Error Handling & Global Exceptions",{"label":2291,"path":2292},"HTTPException vs Custom Exception Classes","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fhttpexception-vs-custom-exception-classes\u002F",{"title":491,"description":2266},"article","CvHuNFAezeuiwFyXO4Mih9JCc-genc-leNehyZIpKSI",[2279,2279],1784588202739]