Module 3: Request and Response

Path Parameters: Validación y Tipos Avanzados

Descripción de la cápsula

En el Módulo 2 usaste path parameters como /items/{item_id} con un type hint int. FastAPI convirtió el valor de la URL a entero automáticamente. Lo que quizá no viste con claridad es qué pasa cuando alguien envía /items/abc o /items/-5 — FastAPI responde con un error 422 (Unprocessable Entity) sin que escribas una línea de validación.

Esta cápsula profundiza en path parameters: validación automática con type annotations, múltiples parámetros en la misma ruta, parámetros con Enum para restringir valores, y el orden correcto entre rutas estáticas y dinámicas. Al terminar dominarás cómo expresar "esta parte de la URL debe ser un entero" o "solo acepto estos valores" usando solo el sistema de tipos de Python.


Path parameters con type annotations

Concepto

Un path parameter es un segmento variable en la URL. En /items/{item_id}, item_id captura lo que venga en esa posición. FastAPI usa el type hint del parámetro para validar y convertir el valor antes de ejecutar tu función.

Ejemplo básico

from fastapi import FastAPI

app = FastAPI()

items = [
    {"id": 1, "name": "Laptop", "price": 999.99},
    {"id": 2, "name": "Mouse", "price": 29.99},
    {"id": 3, "name": "Teclado", "price": 79.99},
]


@app.get("/items/{item_id}")
def get_item(item_id: int):
    item = next((i for i in items if i["id"] == item_id), None)
    if item is None:
        return {"error": "Item not found"}
    return item
  • item_id: int indica a FastAPI que debe convertir el valor a entero
  • Si la conversión falla (ej. /items/abc), FastAPI retorna 422 sin ejecutar tu código

¿Qué pasa con valores inválidos?

Prueba:

curl http://127.0.0.1:8000/items/abc

Respuesta:

{
  "detail": [
    {
      "type": "int_parsing",
      "loc": ["path", "item_id"],
      "msg": "Input should be a valid integer, unable to parse string as an integer",
      "input": "abc"
    }
  ]
}

Status code: 422 Unprocessable Entity. FastAPI valida antes de llamar a tu función.

Otros tipos soportados

# str — por defecto, acepta cualquier texto
@app.get("/users/{username}")
def get_user(username: str):
    return {"username": username}

# float — acepta números decimales
@app.get("/prices/{amount}")
def get_price(amount: float):
    return {"amount": amount}

# bool — convierte "true", "1", "yes" a True; "false", "0", "no" a False
# (Más común en query params, pero funciona en path)

Sobre bool en path params: Aunque técnicamente funciona, en la práctica es raro usarlo. Un path como /items/active/true es poco idiomático. Para filtros booleanos (activo/inactivo, en stock) es más natural usar query params: GET /items?active=true.


Validación automática: el flujo completo

Cuando llega GET /items/3:

  1. FastAPI mapea la ruta a get_item
  2. Extrae "3" como valor de item_id
  3. Intenta convertir "3" a int → éxito → item_id = 3
  4. Ejecuta get_item(item_id=3)
  5. Tu función retorna el item

Cuando llega GET /items/xyz:

  1. FastAPI mapea la ruta a get_item
  2. Extrae "xyz" como valor de item_id
  3. Intenta convertir "xyz" a intfalla
  4. FastAPI retorna 422 con el detalle del error
  5. Tu función nunca se ejecuta

Múltiples path parameters

Una ruta puede tener varios segmentos variables.

Ejemplo: recurso anidado

@app.get("/users/{user_id}/orders/{order_id}")
def get_order(user_id: int, order_id: int):
    return {
        "user_id": user_id,
        "order_id": order_id,
        "message": "Order details"
    }
curl http://127.0.0.1:8000/users/1/orders/42
# → {"user_id": 1, "order_id": 42, "message": "Order details"}

Los nombres de los parámetros deben coincidir con los de la URL. El orden en la firma de la función no importa para FastAPI (usa el nombre).

Ejemplo con str y int

@app.get("/categories/{category}/items/{item_id}")
def get_item_by_category(category: str, item_id: int):
    return {"category": category, "item_id": item_id}
curl http://127.0.0.1:8000/categories/electronica/items/1
# → {"category": "electronica", "item_id": 1}

Path parameters con Enum

A veces quieres restringir los valores posibles. Por ejemplo: /items/{status} donde status solo puede ser "active", "inactive" o "pending".

Definir un Enum

from enum import Enum
from fastapi import FastAPI

app = FastAPI()


class ItemStatus(str, Enum):
    active = "active"
    inactive = "inactive"
    pending = "pending"


@app.get("/items/status/{status}")
def get_items_by_status(status: ItemStatus):
    return {"status": status.value, "message": f"Filtering by {status.value}"}
  • str, Enum permite que FastAPI use los valores del enum en la documentación
  • Si alguien envía /items/status/unknown, FastAPI retorna 422

Probar valores válidos

curl http://127.0.0.1:8000/items/status/active
# → {"status": "active", "message": "Filtering by active"}

Probar valor inválido

curl http://127.0.0.1:8000/items/status/invalid

Respuesta 422:

{
  "detail": [
    {
      "type": "enum",
      "loc": ["path", "status"],
      "msg": "Input should be 'active', 'inactive' or 'pending'",
      "input": "invalid"
    }
  ]
}

Usar el enum en la lógica

items = [
    {"id": 1, "name": "Laptop", "status": "active"},
    {"id": 2, "name": "Mouse", "status": "inactive"},
    {"id": 3, "name": "Teclado", "status": "active"},
]


@app.get("/items/status/{status}")
def get_items_by_status(status: ItemStatus):
    filtered = [i for i in items if i["status"] == status.value]
    return {"status": status.value, "items": filtered, "count": len(filtered)}

¿Por qué heredar de str y Enum?

# Solo Enum — los valores son ItemStatus.active, etc. Más verboso en docs
class ItemStatus(Enum):
    active = "active"
    inactive = "inactive"

# str, Enum — el valor se serializa como string "active". Mejor para APIs REST
class ItemStatus(str, Enum):
    active = "active"
    inactive = "inactive"

Con str, Enum, status.value ya es string y se usa directamente en comparaciones. Sin str, a veces necesitas status.name o conversiones adicionales. Para path y query params en APIs REST, str, Enum es la convención.


Path parameters y documentación automática

FastAPI genera documentación en /docs. Con type hints y Enum:

  • Los path params aparecen como requeridos
  • El Enum muestra un dropdown con los valores posibles
  • Los tipos (int, str) se reflejan en el esquema

Prueba: abre /docs, expande un endpoint con path param y enum. Verás la UI interactiva con las opciones disponibles.


Orden de rutas: estáticas antes que dinámicas

Este concepto ya viste en el Módulo 2, pero es crucial para path params con tipos.

El problema

# ❌ INCORRECTO
@app.get("/items/{item_id}")
def get_item(item_id: int):
    ...

@app.get("/items/latest")
def get_latest():
    ...

/items/latest nunca se ejecuta. FastAPI intenta convertir "latest" a int y falla con 422.

La solución

# ✅ CORRECTO: Rutas estáticas primero
@app.get("/items/latest")
def get_latest():
    return {"message": "Latest item"}


@app.get("/items/{item_id}")
def get_item(item_id: int):
    ...

Regla práctica

  • Rutas con segmentos literales (/items/latest, /items/cheapest) → definir antes
  • Rutas con segmentos variables (/items/{item_id}) → definir después

Resolución de conflictos

Si tienes /items/{item_id} y /items/{slug} (ambos dinámicos), FastAPI no puede distinguirlos por estructura. Usa nombres diferentes en el path si la lógica es distinta, o une la lógica en un solo endpoint y usa el type hint para diferenciar (por ejemplo, int vs str).

Por qué importa el orden

FastAPI recorre las rutas en el orden en que las registras. La primera ruta que coincida se ejecuta. Si /items/{item_id} está primero, cualquier URL /items/algo intentará matchear ahí; "algo" se asigna a item_id y se valida. Por eso las rutas con segmentos literales deben ir antes: así /items/latest matchea la ruta específica y nunca llega a la dinámica.

Depuración: verificar qué ruta matchea

Si sospechas que una ruta no se ejecuta, añade un log o un print temporal. Si nunca aparece en consola, la ruta anterior está capturando la petición. Revisa el orden y asegúrate de que las estáticas estén primero.


Path con FilePath y Path (opcional)

FastAPI ofrece Path() para agregar metadata y validación extra (mínimo, máximo, regex). Por ahora, los type hints son suficientes. En el Módulo 4 verás Field() para Pydantic, que es conceptualmente similar.

Ejemplo rápido para referencia:

from fastapi import Path

@app.get("/items/{item_id}")
def get_item(item_id: int = Path(..., ge=1, le=1000)):
    ...
  • ge=1: mayor o igual a 1
  • le=1000: menor o igual a 1000
  • /items/0 o /items/9999 retornarían 422

Path con descripción para la documentación

from fastapi import Path

@app.get("/items/{item_id}")
def get_item(item_id: int = Path(..., description="ID único del item", ge=1)):
    ...

El ... (Ellipsis) indica que es requerido. La descripción aparece en la documentación de Swagger.


Combinar path params con datos en memoria

En una API real, usarías el item_id para buscar en base de datos. Con datos en memoria:

def find_item(item_id: int):
    return next((i for i in items if i["id"] == item_id), None)

@app.get("/items/{item_id}")
def get_item(item_id: int):
    item = find_item(item_id)
    if item is None:
        return {"error": "Item not found"}
    return item

Extraer la búsqueda a una función ayuda cuando varios endpoints (GET, PUT, PATCH, DELETE) necesitan el mismo recurso.

Patrón: validar existencia antes de operar

En APIs REST completas, sueles tener GET, PUT, PATCH y DELETE para el mismo recurso. En todos necesitas el item_id y validar que exista:

def find_item(item_id: int):
    return next((i for i in items if i["id"] == item_id), None)

@app.get("/items/{item_id}")
def get_item(item_id: int):
    item = find_item(item_id)
    if item is None:
        raise HTTPException(status_code=404, detail="Item not found")
    return item

@app.put("/items/{item_id}")
def update_item(item_id: int, data: ItemUpdate):
    item = find_item(item_id)
    if item is None:
        raise HTTPException(status_code=404, detail="Item not found")
    # actualizar item...
    return item

Usar HTTPException (del módulo fastapi) devuelve un JSON de error estándar. Lo verás en la cápsula de manejo de errores.


Depuración práctica: checklist rápido

Antes de buscar en documentación o Stack Overflow, repasa:

  1. Type hint presente — ¿Tienes item_id: int o solo item_id? Sin tipo, todo llega como string.
  2. Orden de rutas — ¿Las rutas estáticas (/items/latest) están antes que las dinámicas (/items/{item_id})?
  3. Nombres coinciden — En /users/{user_id}/orders/{order_id} la función debe usar user_id y order_id exactamente.
  4. Enum importadofrom enum import Enum y class X(str, Enum).
  5. Ruta registrada — ¿El @app.get(...) está decorando la función correcta y la app está montada?

Con estos cinco puntos cubiertos, la mayoría de problemas con path params se resuelven.

Tip: Si usas UUIDs en lugar de IDs numéricos (ej. /items/550e8400-e29b-41d4-a716-446655440000), declara item_id: UUID importando uuid.UUID de la librería estándar. FastAPI valida y convierte automáticamente.

Path params con UUID

En APIs que exponen recursos sin secuencias numéricas (por seguridad o distribución), UUID es habitual:

from uuid import UUID

@app.get("/items/{item_id}")
def get_item(item_id: UUID):
    # item_id ya es un objeto uuid.UUID validado
    item = next((i for i in items if i["id"] == str(item_id)), None)
    ...

Un valor como /items/not-a-uuid retorna 422 con un mensaje de validación. El formato UUID (8-4-4-4-12 caracteres hex) se valida automáticamente.


Código completo de ejemplo

from enum import Enum
from fastapi import FastAPI

app = FastAPI(
    title="Items API - Path Parameters",
    version="1.0.0",
)

items = [
    {"id": 1, "name": "Laptop", "price": 999.99, "category": "Electrónica"},
    {"id": 2, "name": "Mouse", "price": 29.99, "category": "Accesorios"},
    {"id": 3, "name": "Teclado", "price": 79.99, "category": "Accesorios"},
]


class ItemCategory(str, Enum):
    electronica = "Electrónica"
    accesorios = "Accesorios"
    oficina = "Oficina"


# Rutas estáticas primero
@app.get("/items/latest")
def get_latest_item():
    return max(items, key=lambda i: i["id"])


@app.get("/items/cheapest")
def get_cheapest_item():
    return min(items, key=lambda i: i["price"])


# Ruta con enum
@app.get("/items/category/{category}")
def get_items_by_category(category: ItemCategory):
    filtered = [i for i in items if i["category"] == category.value]
    return {"category": category.value, "items": filtered, "count": len(filtered)}


# Ruta dinámica al final
@app.get("/items/{item_id}")
def get_item(item_id: int):
    item = next((i for i in items if i["id"] == item_id), None)
    if item is None:
        return {"error": "Item not found", "requested_id": item_id}
    return item

Conexión con Proyecto

Los path parameters de esta cápsula son la base de GET /products/{product_id} y GET /products/category/{category} en el proyecto de la Cápsula 06. El uso de Enum para categorías se extiende a filtros por query en ese mismo proyecto.


Errores comunes

Te resumo los fallos más frecuentes al trabajar con path parameters en FastAPI. Reconocerlos te ahorra tiempo de depuración.

  • Anotación de tipo incorrecta o ausente — Si declaras def get_item(item_id) sin type hint, item_id llegará como string siempre. FastAPI no puede validar ni convertir. Usa item_id: int explícitamente para números.
  • Orden incorrecto de rutas — Si defines /items/{item_id} antes que /items/latest, la ruta estática nunca se matchea. FastAPI prueba las rutas en orden y {item_id} captura "latest" intentando convertirlo a int. Siempre define las rutas estáticas primero.
  • Parámetros duplicados en la ruta — No puedes tener dos path params con el mismo nombre, ni usar el mismo nombre para path y query si causan conflicto. Cada {param} debe ser único dentro del path.
  • Enum no importado o mal heredado — Si usas ItemStatus en la firma pero no importas from enum import Enum, tendrás error de definición. Si defines class ItemStatus(Enum) sin str, la documentación y la serialización pueden ser más verbosas. Usa class ItemStatus(str, Enum).
  • Nombre distinto en función y URL — En /users/{user_id}/orders/{order_id} los parámetros deben llamarse exactamente user_id y order_id en la función. Si pones user o uid, FastAPI no los mapeará y obtendrás 422 o un error de argumentos.

Troubleshooting

  • 422 al acceder a ruta estática — La ruta dinámica está definida antes que la estática. Invierte el orden: define /items/latest antes que /items/{item_id}.
  • El path param llega como string — Verifica que uses type hint: item_id: int, no item_id sin tipo. Sin anotación FastAPI no convierte ni valida.
  • Enum no restringe valores — Asegúrate de heredar de str, Enum para que FastAPI muestre los valores en docs y valide correctamente. Comprueba también que el Enum esté importado.
  • Múltiples params no coinciden — Los nombres en la URL deben coincidir con los de la función: /users/{user_id}/orders/{order_id}user_id, order_id. Un typo causa 422.
  • 404 en vez de 422 — Si recibes 404 en lugar de 422 para un valor inválido, puede que la ruta no coincida. Comprueba que el path esté bien registrado y que no haya un middleware que intercepte antes.

Ejercicios

Ejercicio 1: Path param con validación (Fácil)

Crea un endpoint GET /users/{user_id} que retorne {"user_id": N, "name": "User N"}. Verifica que /users/abc retorne 422.

Ver solución
@app.get("/users/{user_id}")
def get_user(user_id: int):
    return {"user_id": user_id, "name": f"User {user_id}"}

Ejercicio 2: Múltiples path params (Fácil)

Implementa GET /projects/{project_id}/tasks/{task_id} que retorne ambos IDs. Prueba con /projects/5/tasks/12.

Ver solución
@app.get("/projects/{project_id}/tasks/{task_id}")
def get_task(project_id: int, task_id: int):
    return {"project_id": project_id, "task_id": task_id}

Ejercicio 3: Enum para prioridad (Medio)

Crea un Enum Priority con valores low, medium, high. Endpoint GET /tasks/priority/{priority} que filtre tareas por prioridad (usa una lista de tareas de ejemplo).

Ver solución
from enum import Enum

class Priority(str, Enum):
    low = "low"
    medium = "medium"
    high = "high"

tasks = [
    {"id": 1, "title": "Fix bug", "priority": "high"},
    {"id": 2, "title": "Document", "priority": "low"},
    {"id": 3, "title": "Review PR", "priority": "medium"},
]

@app.get("/tasks/priority/{priority}")
def get_tasks_by_priority(priority: Priority):
    filtered = [t for t in tasks if t["priority"] == priority.value]
    return {"priority": priority.value, "tasks": filtered}

Ejercicio 4: Rutas estáticas y dinámicas (Medio)

Usando la lista items, implementa:

  • GET /items/expensive — el más caro
  • GET /items/count — número total
  • GET /items/{item_id} — por ID

Verifica que las tres rutas funcionen correctamente.

Ver solución
@app.get("/items/expensive")
def get_expensive_item():
    return max(items, key=lambda i: i["price"])

@app.get("/items/count")
def get_items_count():
    return {"total": len(items)}

@app.get("/items/{item_id}")
def get_item(item_id: int):
    item = next((i for i in items if i["id"] == item_id), None)
    if item is None:
        return {"error": "Item not found"}
    return item

Ejercicio 5: Categorías con Enum (Medio)

Define un Enum ProductCategory con electronics, clothing, books. Lista de productos con category. Endpoint GET /products/category/{category} que filtre y retorne solo productos de esa categoría.

Ver solución
class ProductCategory(str, Enum):
    electronics = "electronics"
    clothing = "clothing"
    books = "books"

products = [
    {"id": 1, "name": "Laptop", "category": "electronics"},
    {"id": 2, "name": "T-Shirt", "category": "clothing"},
    {"id": 3, "name": "Python Book", "category": "books"},
]

@app.get("/products/category/{category}")
def get_products_by_category(category: ProductCategory):
    filtered = [p for p in products if p["category"] == category.value]
    return {"category": category.value, "products": filtered}

Ejercicio 6: Diagnóstico 422 (Difícil)

Un desarrollador tiene este código y recibe 422 en GET /items/new. ¿Cuál es el problema?

@app.get("/items/{item_id}")
def get_item(item_id: int):
    ...

@app.get("/items/new")
def get_new_items():
    ...
Ver solución

La ruta dinámica /items/{item_id} está definida antes que la estática /items/new. FastAPI intenta convertir "new" a int y falla. Solución: definir /items/new antes de /items/{item_id}.


Resumen

  • Los path parameters se declaran con {param} en la ruta y el mismo nombre en la función
  • Los type hints (int, str, float) activan validación y conversión automática — valores inválidos retornan 422
  • Múltiples path params en la misma ruta funcionan igual; los nombres deben coincidir
  • Enum restringe valores posibles; str, Enum es la forma recomendada
  • Rutas estáticas deben definirse antes que rutas dinámicas con el mismo prefijo

Próxima cápsula: Query parameters — skip, limit, filtros opcionales y paginación.


Recursos Adicionales

  1. FastAPI - Path Parameters - Documentación oficial
  2. Python enum — Enumeration type - Módulo Enum
  3. HTTP 422 Unprocessable Entity - Significado del status
  4. FastAPI - Path Parameters with Path() - Validaciones numéricas con Path
  5. REST Resource Naming - Convenciones de nombres para recursos REST
  6. Pydantic Type System - Tipos soportados para validación (compartido con FastAPI)

Módulo 3, Cápsula 02 — FastAPI Fundamentals Guide