Elegir espacio

Dos espacios

¿Qué quieres consultar?

Elige el espacio al que quieres entrar.

Recorrido · FastAPIBloque II · Unidad 2.4

Bloque II · Módulo 3 · Unidad 2.4

Respuestas, errores y contrato observable

Controlar representación pública, estados, headers y errores consistentes sin filtrar datos internos ni degradar el diagnóstico.

1. El contrato observable#

Un cliente no observa tus clases internas. Observa método, URL, status code, headers, media type y body. Un endpoint es correcto cuando esos elementos expresan de forma coherente el resultado y permanecen compatibles con lo documentado.

Separar contratos de entrada, dominio temporal y salida evita que un cambio interno publique accidentalmente campos nuevos.

2. Filtrar y validar con response_model#

from pydantic import BaseModel

class RecordInternal(BaseModel):
    id: str
    subject: str
    owner_email: str
    audit_token: str

class RecordPublic(BaseModel):
    id: str
    subject: str

@app.get("/expedientes/{record_id}", response_model=RecordPublic)
def read_record(record_id: str):
    return RecordInternal(
        id=record_id,
        subject="Alta interna",
        owner_email="owner@example.invalid",
        audit_token="internal-only",
    )

FastAPI usa response_model para documentar, serializar, validar y filtrar la salida. El cliente recibe solo id y subject. Esto no reemplaza una revisión de seguridad, pero convierte la representación pública en una lista permitida.

FronteraFallo típicoResponsableRespuesta
Request modelEl cliente envía un tipo inválido.Cliente.422 con ubicación útil.
Regla de negocioReferencia duplicada.Estado/operación.409 con error público.
Response modelLa aplicación devuelve un dato incompatible.Servidor.Error interno; corregir implementación.

3. Status code y headers#

from fastapi import Response, status

@app.post(
    "/expedientes",
    response_model=RecordPublic,
    status_code=status.HTTP_201_CREATED,
)
def create_record(payload: RecordCreate, response: Response):
    created = create_in_memory(payload)
    response.headers["Location"] = f"/expedientes/{created.id}"
    return created

201 Created comunica creación y Location identifica el recurso creado. Declarar status_code en el decorador lo incorpora a OpenAPI. Modificar el objeto Response inyectado permite añadir headers sin renunciar al filtrado del modelo.

Respuesta HTTP

4. Errores de negocio consistentes#

HTTPException interrumpe la operación y produce una respuesta:

from fastapi import HTTPException

if reference_exists(payload.reference):
    raise HTTPException(
        status_code=409,
        detail={
            "code": "record_reference_conflict",
            "message": "The record reference is already in use",
        },
    )

Para evitar que unas operaciones devuelvan texto y otras diccionarios distintos, define un modelo:

class ErrorDetail(BaseModel):
    code: str
    message: str
    field: str | None = None

class ErrorEnvelope(BaseModel):
    error: ErrorDetail

Puedes crear una excepción de dominio pequeña y un manejador global que siempre produzca ErrorEnvelope. El dominio expresa el problema; el manejador decide el transporte HTTP.

5. Personalizar errores de validación sin perder señal#

from fastapi import Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

@app.exception_handler(RequestValidationError)
async def validation_handler(
    request: Request,
    exc: RequestValidationError,
) -> JSONResponse:
    issues = [
        {
            "location": list(error["loc"]),
            "code": error["type"],
            "message": error["msg"],
        }
        for error in exc.errors()
    ]
    return JSONResponse(
        status_code=422,
        content={
            "error": {
                "code": "request_validation_failed",
                "message": "The request does not satisfy the contract",
                "issues": issues,
            }
        },
    )

La transformación conserva loc, type y un mensaje útil, pero omite la entrada cruda si podría contener secretos. No incluyas request.body() ni str(exc) sin una política explícita.

6. La operación también se documenta#

@app.post(
    "/expedientes",
    response_model=RecordPublic,
    status_code=201,
    summary="Create an internal record",
    operation_id="createRecord",
    tags=["records"],
    responses={
        409: {
            "model": ErrorEnvelope,
            "description": "Reference already exists",
        }
    },
)
def create_record(payload: RecordCreate):
    ...

El decorador anterior alimenta un documento con estructura OAS real. Esta vista YAML acotada muestra la raíz y los objetos principales que debes poder localizar:

openapi: 3.1.0
info:
  title: API interna de expedientes
  version: 0.1.0
paths:
  /expedientes:
    post:
      summary: Crear un expediente interno
      operationId: createRecord
      tags: [records]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RecordCreate"
      responses:
        "201":
          description: Expediente creado
          headers:
            Location:
              description: URI del expediente creado
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecordPublic"
        "409":
          description: La referencia ya existe
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
components:
  schemas:
    RecordCreate:
      type: object
      required: [reference, subject, priority]
      properties:
        reference:
          type: string
        subject:
          type: string
        priority:
          type: integer
          minimum: 1
          maximum: 5
    RecordPublic:
      type: object
      required: [id, subject]
      properties:
        id:
          type: string
        subject:
          type: string
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          $ref: "#/components/schemas/ErrorDetail"
    ErrorDetail:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
        message:
          type: string

Las claves fijas permanecen en inglés porque forman parte del vocabulario case-sensitive de OpenAPI; title, summary y description contienen texto humano y pueden localizarse. FastAPI añadirá además la respuesta de validación 422 cuando la operación acepte entrada validable.

Revisa /openapi.json, no solo /docs. El documento debe mostrar request body, respuesta 201, esquema público, 409 y 422. operation_id debe ser único y estable si clientes generan código.

7. Cuándo una Response directa cambia el contrato#

from fastapi.responses import PlainTextResponse

@app.get("/health/raw", response_class=PlainTextResponse)
def raw_health():
    return "ok"

Una Response directa permite controlar body, media type, cookies y headers. Pero si devuelves un objeto Response construido manualmente, FastAPI no aplica sobre ese body el mismo pipeline de conversión y filtrado de un response_model.

Úsala cuando la representación no sea JSON o necesites control explícito. Para JSON de dominio, prefiere tipos y modelos declarativos.

8. Criterios de dominio#

  • El modelo público filtra campos internos.
  • Creación devuelve 201 y un Location coherente.
  • Errores de negocio tienen código estable y estado defendible.
  • El manejador de validación conserva ubicación sin filtrar input sensible.
  • OpenAPI describe éxito y errores relevantes.
  • Una respuesta manual se usa de forma consciente y documentada.
  • No se confunden fallos de entrada con bugs de salida.

Prácticas relacionadas#

Fuentes#