Saltar al contenido
🐍 Curso de Python
/

Módulo 5 · Lección 24 de 30

Rutas y parámetros

Cómo decide FastAPI de dónde sale cada dato, la ruta que deja de funcionar por declararla en el orden equivocado, y la anotación que impide publicar una contraseña.

⏱ 15 min de lectura

En DRF hay que decir de dónde sale cada cosa: self.kwargs['pk'] para la ruta, request.query_params para la consulta, request.data para el cuerpo. En FastAPI se declaran como argumentos de una función y el armazón decide.

Esa decisión sigue tres reglas, y merece la pena saberlas de memoria porque el resto de la lección depende de ellas.

De dónde sale cada parámetro#

python
@app.get('/r/{en_ruta}')
def todo(en_ruta: int,
         simple: str = 'por-defecto',
         lista: list[int] = Query(default=[]),
         cab: str | None = Header(default=None),
         galleta: str | None = Cookie(default=None)):
        ...
Salida en pantalla
GET /r/42?simple=hola&lista=1&lista=2
con la cabecera cab: valor y la galleta galleta=g

{"en_ruta": 42, "simple": "hola", "lista": [1, 2],
 "cab": "valor", "galleta": "g"}

Las tres reglas, por orden:

Si el nombre aparece entre llaves en la ruta, sale de la ruta.

Si el tipo es un modelo de pydantic, sale del cuerpo.

Si no es ninguna de las dos, sale de la cadena de consulta.

Y para lo demás —cabeceras, galletas, formularios, ficheros— hay funciones que lo dicen explícitamente.

Combinando las tres en una sola función:

Salida en pantalla
POST /c/7?q=zzz  con cuerpo {"nombre": "Ana"}

{"id": 7, "cuerpo": {"nombre": "Ana"}, "q": "zzz"}

El id vino de la ruta, cuerpo del cuerpo porque es un modelo, y q de la consulta porque es un texto suelto. Sin configurar nada.

Fíjate en el detalle de la lista: lista: list[int] recoge ?lista=1&lista=2 y devuelve [1, 2] ya convertidos a enteros. Es lo que la lección 6 vio desde el lado del cliente, resuelto aquí sin código.

Validar los parámetros#

Las restricciones van con el tipo:

python
from typing import Annotated

from fastapi import Path, Query


@app.get('/api/libros/{libro_id}')
def obtener(
    libro_id: Annotated[int, Path(ge=1)],
    q: Annotated[str | None, Query(min_length=3, max_length=50)] = None,
    limite: Annotated[int, Query(ge=1, le=100)] = 20,
    orden: Annotated[str, Query(pattern='^-?(titulo|precio)$')] = 'titulo',
):
        ...

Cuatro cosas que esa firma consigue de golpe, y que en DRF son la lección 19 entera:

El límite tiene tope, que es el aviso de la lección 20 sobre pedir un millón de elementos.

El orden solo acepta lo que has puesto, que es el aviso de la lección 19 sobre no permitir ordenar por cualquier columna. Aquí la lista blanca es la expresión regular.

Todo aparece en la documentación con sus restricciones.

Y un valor inválido da error antes de entrar en tu función.

La diferencia con la lección 19 es importante: allí, un parámetro con una errata se ignoraba en silencio. Aquí, un valor inválido da error igual que en DRF; un nombre desconocido también se ignora, porque una cadena de consulta puede llevar cualquier cosa. La defensa vuelve a ser documentarlo, y aquí sale gratis.

Organizar la API#

Un fichero no aguanta. La pieza es el enrutador:

python
# app/routers/libros.py
from fastapi import APIRouter

router = APIRouter(prefix='/api/libros', tags=['Libros'])


@router.get('/')
def listar() -> list[Libro]:
        ...


@router.get('/{libro_id}')
def obtener(libro_id: int) -> Libro:
        ...
python
# app/main.py
from fastapi import FastAPI

from .routers import autores, libros

app = FastAPI(title='API del catálogo')
app.include_router(libros.router)
app.include_router(autores.router)

Es el equivalente de las aplicaciones y el include de Django, con dos diferencias: el prefijo y las etiquetas van en el enrutador y no en quien lo incluye, y las dependencias comunes también:

python
router = APIRouter(
    prefix='/api/libros',
    tags=['Libros'],
    dependencies=[Depends(exigir_token)],
    responses={404: {'description': 'No encontrado'}},
)

Esa línea de dependencias aplica la autenticación a todo el enrutador de una vez, que es lo que en DRF hacía permission_classes en la clase. Es la lección 27.

La anotación de retorno es un filtro#

Aquí está la mejor característica de esta lección, y es de seguridad.

python
class UsuarioBD(BaseModel):
        id: int
    correo: str
    contrasena_cifrada: str
    es_admin: bool
    notas_internas: str


class UsuarioPublico(BaseModel):
        id: int
    correo: str
python
@app.get('/sin-anotar')
def sin_anotar():
        return usuario


@app.get('/anotado')
def anotado() -> UsuarioPublico:
        return usuario
Salida en pantalla
/sin-anotar -> {"id": 1, "correo": "ana@t.es",
                "contrasena_cifrada": "pbkdf2$xxxx",
                "es_admin": true,
                "notas_internas": "cliente difícil"}

/anotado    -> {"id": 1, "correo": "ana@t.es"}

La anotación recorta. Y no es que avise ni que dé error: los campos que no están declarados en el modelo de salida no salen, aunque el objeto que devuelves los tenga.

Funciona incluso devolviendo un diccionario con campos de más:

Salida en pantalla
return {'id': 1, 'correo': 'a@t.es', 'contrasena_cifrada': 'xxx', 'es_admin': True}
-> {"id": 1, "correo": "a@t.es"}

Esto es lo que en DRF era la lista explícita de campos de la lección 14, con dos diferencias a favor:

Se aplica al valor devuelto, no al objeto de entrada. Da igual de dónde venga el dato o si te equivocas al construirlo: si no está en el modelo de salida, no se publica.

Y es la misma declaración que documenta. No puede desalinearse, que es lo que la lección anterior señalaba como su mejor propiedad.

Y de ahí la regla que sí es importante: anota siempre el retorno. Una función sin anotación devuelve el objeto entero, que es el fields = '__all__' de la lección 14 con la misma consecuencia: publicas el campo que alguien añada mañana.