Elegir espacio

Dos espacios

¿Qué quieres consultar?

Elige el espacio al que quieres entrar.

Recorrido · FastAPIBloque II · Unidad 2.2

Bloque II · Módulo 2 · Unidad 2.2

Entrada de datos HTTP y validación automática

Declarar de dónde procede cada dato de una petición y comprender conversión, obligatoriedad, ausencia, null y errores 422.

1. Una firma es un contrato#

FastAPI interpreta nombres, tipos y metadatos de la firma. Cada parámetro declara dos decisiones diferentes: de dónde se extrae el dato y qué valores se aceptan.

from typing import Annotated
from fastapi import FastAPI, Header, Path, Query

app = FastAPI()

@app.get("/expedientes/{expediente_id}")
def read_record(
    expediente_id: Annotated[str, Path(pattern=r"^EXP-\d{3}$")],
    page: Annotated[int, Query(ge=1)] = 1,
    correlation_id: Annotated[str | None, Header(alias="X-Correlation-ID")] = None,
):
    return {"id": expediente_id, "page": page, "correlation_id": correlation_id}

Annotated[T, Query(...)] conserva T como tipo para Python y adjunta metadatos para FastAPI. El valor por defecto sigue expresando obligatoriedad: sin default es requerido; = 1 lo hace opcional con default; = None permite ausencia.

2. Path y query#

El path identifica el recurso o subrecurso que se está direccionando. La query modifica una vista: filtra, pagina, ordena o incluye representaciones opcionales.

PreguntaPathQuery
¿Qué expresa?Identidad dentro de la ruta.Opciones sobre la representación o colección.
¿Puede omitirse?No si forma parte del patrón.Sí, cuando existe default.
Ejemplo/expedientes/EXP-204?estado=abierto&page=2
Error de diseñoMeter un filtro cambiante en la jerarquía.Usarla para identificar ambiguamente el recurso principal.
@app.get("/expedientes")
def list_records(
    estado: Annotated[str | None, Query(pattern="^(abierto|cerrado)$")] = None,
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
):
    ...

Los valores de query llegan como texto. FastAPI los convierte según el tipo y produce un error de validación si la conversión o las restricciones fallan.

3. Headers y cookies#

Los headers transportan metadatos de la interacción: negociación, autenticación, trazabilidad o condiciones. Las cookies son pares gestionados por el cliente para un dominio y deben usarse con cautela.

from fastapi import Cookie

@app.get("/preferencias")
def preferences(
    correlation_id: Annotated[str, Header(alias="X-Correlation-ID")],
    display_mode: Annotated[str | None, Cookie(alias="display_mode")] = None,
):
    ...

FastAPI convierte normalmente guiones bajos de un parámetro de header en guiones. Un alias explícito hace visible el nombre público y evita depender de esa convención.

4. Body JSON#

Un modelo Pydantic en la firma se interpreta como body JSON:

from pydantic import BaseModel, Field

class RecordCreate(BaseModel):
    subject: str = Field(min_length=3, max_length=120)
    priority: int = Field(ge=1, le=5)

@app.post("/expedientes")
def create_record(payload: RecordCreate):
    return payload
Petición HTTP

El modelo agrupa campos relacionados de una representación. No uses un body para todos los datos: un identificador que direcciona el recurso sigue perteneciendo al path; el correlation ID sigue siendo un header.

5. Formulario y archivo#

Los formularios HTML envían application/x-www-form-urlencoded o multipart/form-data. Los archivos requieren multipart:

from fastapi import File, Form, UploadFile

@app.post("/importaciones")
async def import_record(
    category: Annotated[str, Form()],
    document: Annotated[UploadFile, File()],
):
    content = await document.read(1024)
    return {"category": category, "filename": document.filename, "sample_bytes": len(content)}

UploadFile ofrece metadatos y un archivo temporal con interfaz asíncrona; evita cargar de inmediato todo el contenido como bytes. Debes imponer límites reales en una aplicación de producción, pero ese diseño pertenece a un alcance posterior.

6. Ausente, null y default no son lo mismo#

Considera:

class RecordPatch(BaseModel):
    title: str | None = None

Este tipo permite que title se omita y que se envíe null; en ambos casos el atributo vale None. Para una actualización parcial necesitas además saber si el campo fue enviado:

payload.model_fields_set
payload.model_dump(exclude_unset=True)
EntradaPresenciaValorUso típico
{}AusenteDefault en el modeloNo cambiar el campo.
{"title": null}PresenteNoneVaciarlo si el dominio lo permite.
{"title": "Nueva"}PresenteTexto validadoReemplazarlo.

El tipo str | None permite None; no convierte por sí solo el campo en opcional. La presencia se decide con el default:

required_but_nullable: str | None
optional_and_nullable: str | None = None

7. Leer un error de validación#

Cuando path, query, header o body no cumplen el contrato, FastAPI genera una respuesta 422. Cada elemento incluye una ubicación (loc), un mensaje (msg), un tipo de error y, según el caso, contexto e input.

{
  "detail": [
    {
      "type": "greater_than_equal",
      "loc": ["query", "page"],
      "msg": "Input should be greater than or equal to 1",
      "input": "0",
      "ctx": {"ge": 1}
    }
  ]
}

La ubicación permite corregir el cliente sin adivinar: ["query", "page"] no es lo mismo que ["body", "page"].

8. Diseñar la frontera antes de codificar#

Para cada dato pregunta:

  1. ¿identifica el recurso? → path;
  2. ¿filtra o pagina? → query;
  3. ¿describe la interacción? → header;
  4. ¿es una preferencia pequeña del cliente? → cookie;
  5. ¿forma una representación estructurada? → body JSON;
  6. ¿procede de un formulario o acompaña un archivo? → form/multipart.

Después declara tipo, obligatoriedad, límites y ejemplo. Solo entonces escribe la operación.

9. Criterios de dominio#

  • Justificas la fuente HTTP de cada dato.
  • Predices si un parámetro es requerido, opcional o nullable.
  • Distingues JSON, formulario y multipart.
  • Lees loc y reconoce qué validación falló.
  • Preservas la diferencia entre omitido y null.
  • Evitas transportar secretos en cookies o errores.

Prácticas relacionadas#

Fuentes#