Recommended resources
The freely available resources this lesson will direct you to.
Hello! Your incident API now has a deliberate public shape for successful responses. The next question is what clients receive when there is no incident to return, or when the request is valid but the requested operation cannot be performed.
In this lesson, you’ll give those two situations distinct status codes and a predictable error body. You’ll also check that the documented errors match what the API actually sends. Allow about 40 minutes, including the short video, reading, implementation, and checks.
An error is part of the API contract
Suppose a client requests incident 999, which does not exist. Returning null with a successful status would force the client to guess whether the lookup succeeded. A 404 Not Found states the result directly. By contrast, suppose incident 42 exists but has already been archived. Repeating an archive operation is a conflict with its current state, so this example will return 409 Conflict.
Watch the missing-item example in JVP Design’s Fast API Tutorial, Part 19: Handling Errors. Focus on the difference between raising HTTPException and returning an ordinary value.
Fast API Tutorial, Part 19: Handling Errors
JVP Design demonstrates how a lookup becomes an HTTP error when the requested item is absent.
Watch the missing item, from the dictionary lookup through the request that receives a 404. Notice that the exception stops normal handling and supplies both a status code and a response detail.
FastAPI’s Handling Errors documentation adds an important option: detail need not be a string. We can use a small JSON-compatible object to give clients a stable error code as well as a readable message.
Read FastAPI’s explanation of raising HTTPException and the response it produces. This is the mechanism we’ll use for both incident errors.
In “Use HTTPException,” begin at the “Raise an HTTPException in your code” subsection. Read the explanation and example through “The resulting response” and its tip about detail. Focus on why handling stops at raise, and why a dictionary supplied as detail can become JSON.
We’ll use the same body shape for both domain errors:
{
"detail": {
"code": "incident_not_found",
"message": "Incident not found"
}
}
The status tells an HTTP client what kind of failure occurred. The code is a stable value application code can inspect. The message is safe, human-readable context. Neither needs to expose an internal note, routing token, or exception traceback.
Add a small stateful example
The previous POST handler returned a public incident with a placeholder ID but did not store it. To demonstrate a real missing-resource check, add a small in-memory incident store. This is only for learning the HTTP behavior: it resets when the process restarts and is not a substitute for the database you’ll add later.
The code below belongs in your existing app module. Keep its existing app = FastAPI() and schema definitions; adjust the import location or enum name if yours differs. Replace the previous demonstration POST handler rather than registering a second POST /incidents. Leave the earlier demonstration PATCH handler alone for now.
from itertools import count
from fastapi import HTTPException
from pydantic import BaseModel
from incident_api.schemas import IncidentCreate, IncidentPublic, IncidentSeverity
class ErrorDetail(BaseModel):
code: str
message: str
class ErrorResponse(BaseModel):
detail: ErrorDetail
INCIDENTS: dict[int, IncidentPublic] = {
42: IncidentPublic(
id=42,
title="Deployment fails",
severity=IncidentSeverity("high"),
description="Alerts firing",
)
}
ARCHIVED_IDS: set[int] = set()
NEXT_ID = count(43)
def require_incident(incident_id: int) -> IncidentPublic:
incident = INCIDENTS.get(incident_id)
if incident is None:
raise HTTPException(
status_code=404,
detail={
"code": "incident_not_found",
"message": "Incident not found",
},
)
return incident
@app.post("/incidents", response_model=IncidentPublic, status_code=201)
def create_incident(incident: IncidentCreate) -> dict[str, object]:
record: dict[str, object] = {
**incident.model_dump(),
"id": next(NEXT_ID),
"internal_notes": "Internal triage only",
"routing_token": "example-only",
}
public_incident = IncidentPublic.model_validate(record)
INCIDENTS[public_incident.id] = public_incident
return record
@app.get(
"/incidents/{incident_id}",
response_model=IncidentPublic,
responses={404: {"model": ErrorResponse}},
)
def read_incident(incident_id: int) -> IncidentPublic:
return require_incident(incident_id)
@app.post(
"/incidents/{incident_id}/archive",
status_code=204,
responses={
404: {"model": ErrorResponse},
409: {"model": ErrorResponse},
},
)
def archive_incident(incident_id: int) -> None:
require_incident(incident_id)
if incident_id in ARCHIVED_IDS:
raise HTTPException(
status_code=409,
detail={
"code": "incident_already_archived",
"message": "Incident is already archived",
},
)
ARCHIVED_IDS.add(incident_id)
There are two useful boundaries here. First, require_incident() centralizes lookup behavior: both routes return the same 404 for the same missing incident. Second, the archive route checks existence before checking whether archiving is allowed. An unknown ID is not an “already archived” incident.
The archive route is a focused demonstration of an invalid operation, not a complete incident lifecycle. Archival is kept as internal example state, and a successful archive returns 204 No Content. Later, you’ll model statuses and allowed transitions explicitly rather than relying on a set in memory. The earlier PATCH handler still echoes proposed changes; it is not connected to this store.
Keep documentation and runtime behavior aligned
The responses entries above describe additional responses in OpenAPI. They do not cause a 404 or 409 by themselves. The raised HTTPException determines the runtime status and body; ErrorResponse describes that body to documentation readers.
Read the beginning of FastAPI’s Additional Responses in OpenAPI to see that distinction. Its example directly returns a JSONResponse; our routes instead raise HTTPException, which FastAPI handles to produce the error response.
Additional Responses in OpenAPI - FastAPI
Read the opening and “Additional Response with model” in FastAPI’s advanced guide. This explains what the responses parameter documents—and what it does not enforce.
Starting with the introductory explanation, read additional responses through the explanation of the model entry. Note the warning that the developer must still produce the promised status code and content at runtime.
The documentation image shows the intended result for a different GET route: the successful response and the not-found response appear separately.

After restarting your development server, use these requests to inspect the wire responses, not just /docs:
curl -i 'http://127.0.0.1:8000/incidents/42'
curl -i 'http://127.0.0.1:8000/incidents/999'
curl -i -X POST 'http://127.0.0.1:8000/incidents/42/archive'
curl -i -X POST 'http://127.0.0.1:8000/incidents/42/archive'
In that order, expect 200 with the public incident, 404 with incident_not_found, 204 with no response body, and 409 with incident_already_archived. Restarting the process clears the archived state, so run the two archive requests in one server session. Then inspect the GET and archive operations in /docs: their listed error schemas should describe the detail object your requests actually received.
A malformed path ID, such as /incidents/not-an-integer, is different again. FastAPI rejects it during request validation, normally with 422 and its default validation-error body. We have made the missing-resource and invalid-operation errors consistent with each other; we have not claimed to replace every framework-generated client error. Likewise, an unexpected bug should not be disguised as a 404 or 409 merely to preserve an error shape.
Takeaways
Use 404 when the incident is absent and 409 when an existing incident’s state prevents the requested operation. Raise HTTPException with a stable, safe detail shape; reuse lookup behavior so routes agree about missing resources. Finally, treat OpenAPI responses as documentation to verify against actual responses, not as runtime enforcement.
Next, you’ll refine the generated OpenAPI documentation with operation tags, summaries, and request examples so clients can navigate this growing API more easily.