Module 3: Request and Response
Path(), Query(), Body() — Control Avanzado de Parámetros
Descripción de la cápsula
En las cápsulas anteriores aprendiste a recibir path parameters con type hints, query parameters con valores por defecto, y request body con Body(...). Todo eso funciona — pero es como usar un destornillador básico cuando existe uno eléctrico. FastAPI incluye las funciones Path(), Query() y Body() que te dan control fino sobre cada parámetro: descripciones que aparecen en /docs, alias para query strings, constraints de validación (valores mínimos, máximos, longitudes), marcado de deprecación, y ejemplos embebidos en la documentación.
¿Por qué importa? Porque una API profesional no solo funciona — se documenta sola. Cuando usas Query(description="Filter by genre"), cualquier developer que abra /docs entiende qué hace ese parámetro sin leer tu código. Cuando usas Path(ge=1), FastAPI rechaza automáticamente IDs negativos con un error 422 claro, sin que escribas un solo if. Son herramientas que convierten tus endpoints de "funciona" a "funciona y se explica solo."
Al terminar esta cápsula sabrás cuándo usar type hints simples y cuándo necesitas Path(), Query() o Body(), cómo configurar validación numérica y de strings, cómo combinar los tres tipos de parámetros en un solo endpoint, y cómo luce todo esto en la documentación automática.
Por qué existen Path(), Query() y Body()
Type hints simples: Lo que ya sabes
Hasta ahora has escrito endpoints así:
from fastapi import FastAPI
app = FastAPI()
books = [
{"id": 1, "title": "Cien años de soledad", "author": "Gabriel García Márquez", "year": 1967, "genre": "Realismo mágico"},
{"id": 2, "title": "Don Quijote de la Mancha", "author": "Miguel de Cervantes", "year": 1605, "genre": "Novela"},
{"id": 3, "title": "Rayuela", "author": "Julio Cortázar", "year": 1963, "genre": "Novela experimental"},
{"id": 4, "title": "La casa de los espíritus", "author": "Isabel Allende", "year": 1982, "genre": "Realismo mágico"},
{"id": 5, "title": "Ficciones", "author": "Jorge Luis Borges", "year": 1944, "genre": "Cuentos"},
]
@app.get("/books/{book_id}")
def get_book(book_id: int):
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return {"error": "Book not found"}
return book
book_id: int funciona. FastAPI sabe que viene de la URL, valida que sea entero, y listo. Pero no tienes forma de decirle "el ID debe ser positivo", ni de agregar una descripción que aparezca en /docs, ni de definir un ejemplo. El type hint solo le dice qué tipo es — no le dice qué reglas debe cumplir.
Las funciones de control: Lo que vas a aprender
Path(), Query() y Body() son funciones que te permiten agregar metadatos y restricciones a cada parámetro. Piensa en ellas como "type hints con superpoderes":
from fastapi import FastAPI, Path, Query, Body
Cada una se usa como valor por defecto del parámetro:
def get_book(book_id: int = Path(..., ge=1, description="ID del libro")):
En lugar de solo book_id: int, ahora tienes: es requerido (...), debe ser mayor o igual a 1 (ge=1), y aparece con descripción en /docs.
Query() — Control de query parameters
Ejemplo básico: Descripciones para documentación
Toma el endpoint de filtrar libros que ya conoces y agrégale Query():
from fastapi import FastAPI, Query
app = FastAPI()
books = [
{"id": 1, "title": "Cien años de soledad", "author": "Gabriel García Márquez", "year": 1967, "genre": "Realismo mágico"},
{"id": 2, "title": "Don Quijote de la Mancha", "author": "Miguel de Cervantes", "year": 1605, "genre": "Novela"},
{"id": 3, "title": "Rayuela", "author": "Julio Cortázar", "year": 1963, "genre": "Novela experimental"},
{"id": 4, "title": "La casa de los espíritus", "author": "Isabel Allende", "year": 1982, "genre": "Realismo mágico"},
{"id": 5, "title": "Ficciones", "author": "Jorge Luis Borges", "year": 1944, "genre": "Cuentos"},
]
@app.get("/books")
def list_books(
genre: str = Query(default=None, description="Filtrar por género literario"),
limit: int = Query(default=10, description="Máximo de resultados a retornar"),
):
results = books
if genre:
results = [b for b in results if b["genre"].lower() == genre.lower()]
return results[:limit]
Sin Query(), esos parámetros aparecen en /docs como simples campos sin contexto. Con Query(description=...), cada parámetro tiene una explicación clara. Abre http://127.0.0.1:8000/docs, expande GET /books, y verás las descripciones junto a cada campo.
curl "http://127.0.0.1:8000/books?genre=Novela&limit=2"
Output esperado:
[
{"id": 2, "title": "Don Quijote de la Mancha", "author": "Miguel de Cervantes", "year": 1605, "genre": "Novela"}
]
Ejemplo intermedio: Constraints de validación
Aquí es donde Query() brilla. Puedes restringir valores numéricos y longitud de strings. Usando el mismo setup de app y books:
@app.get("/books")
def list_books(
genre: str = Query(default=None, description="Filtrar por género literario"),
limit: int = Query(default=10, ge=1, le=100, description="Máximo de resultados (1-100)"),
search: str = Query(default=None, min_length=2, max_length=50, description="Buscar en títulos"),
):
results = books
if genre:
results = [b for b in results if b["genre"].lower() == genre.lower()]
if search:
results = [b for b in results if search.lower() in b["title"].lower()]
return results[:limit]
Los constraints:
| Constraint | Aplica a | Significado |
|---|---|---|
ge=1 | Números | Greater than or Equal — mínimo 1 |
le=100 | Números | Less than or Equal — máximo 100 |
gt=0 | Números | Greater Than — estrictamente mayor que 0 |
lt=1000 | Números | Less Than — estrictamente menor que 1000 |
min_length=2 | Strings | Longitud mínima de 2 caracteres |
max_length=50 | Strings | Longitud máxima de 50 caracteres |
Prueba violar un constraint:
curl "http://127.0.0.1:8000/books?limit=0"
Output — FastAPI retorna 422 automáticamente:
{
"detail": [
{
"type": "greater_than_equal",
"loc": ["query", "limit"],
"msg": "Input should be greater than or equal to 1",
"input": "0",
"ctx": {"ge": 1}
}
]
}
No escribiste ningún if limit < 1. FastAPI validó por ti gracias a ge=1. El error es claro: dice qué parámetro falló (limit), de dónde viene (query), y cuál es la regla (ge: 1).
curl "http://127.0.0.1:8000/books?search=a"
Output — falla por min_length=2:
{
"detail": [
{
"type": "string_too_short",
"loc": ["query", "search"],
"msg": "String should have at least 2 characters",
"input": "a",
"ctx": {"min_length": 2}
}
]
}
Ejemplo avanzado: Alias y deprecación
Query() tiene dos opciones más que verás en APIs reales:
@app.get("/books")
def list_books(
genre: str = Query(default=None, description="Filtrar por género"),
limit: int = Query(default=10, ge=1, le=100, description="Máximo de resultados"),
search: str = Query(default=None, min_length=2, max_length=50, description="Buscar en títulos"),
sort_by: str = Query(default=None, alias="sort-by", description="Campo para ordenar"),
old_filter: str = Query(default=None, deprecated=True, description="Usar 'genre' en su lugar"),
):
results = books
if genre:
results = [b for b in results if b["genre"].lower() == genre.lower()]
if search:
results = [b for b in results if search.lower() in b["title"].lower()]
return results[:limit]
alias="sort-by"— El cliente envía?sort-by=title, pero en tu código usassort_by(Python no permite guiones en nombres de variables). El alias conecta el nombre de la URL con el nombre de Python.deprecated=True— En/docs, este parámetro aparece tachado y marcado como deprecated. Los clientes ven que deben dejar de usarlo.
curl "http://127.0.0.1:8000/books?sort-by=title"
Path() — Control de path parameters
Ejemplo básico: Descripción y validación
Agrega Path al import (from fastapi import FastAPI, Path) y modifica el endpoint de obtener libro por ID:
@app.get("/books/{book_id}")
def get_book(
book_id: int = Path(..., ge=1, description="ID del libro, debe ser positivo"),
):
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return {"error": "Book not found"}
return book
Los tres puntos (...) significan que el parámetro es requerido — que en el caso de path parameters siempre lo es (viene de la URL). ge=1 garantiza que nadie pase un ID de 0 o negativo.
curl http://127.0.0.1:8000/books/3
Output esperado:
{"id": 3, "title": "Rayuela", "author": "Julio Cortázar", "year": 1963, "genre": "Novela experimental"}
curl http://127.0.0.1:8000/books/0
Output — rechazado por ge=1:
{
"detail": [
{
"type": "greater_than_equal",
"loc": ["path", "book_id"],
"msg": "Input should be greater than or equal to 1",
"input": "0",
"ctx": {"ge": 1}
}
]
}
Sin Path(ge=1), el request pasaría y tu función buscaría un libro con id=0 — no lo encontraría, pero el error sería menos claro. Con Path(), FastAPI intercepta el valor inválido antes de que tu código se ejecute.
Prueba también con /books/-5 — mismo rechazo. Cualquier valor menor a 1 es interceptado antes de que tu código se ejecute.
Ejemplo intermedio: Path con título para docs
@app.get("/books/{book_id}")
def get_book(
book_id: int = Path(
...,
ge=1,
le=10000,
title="Book ID",
description="Identificador único del libro, positivo y menor a 10000",
),
):
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return {"error": "Book not found"}
return book
title y description aparecen en la documentación OpenAPI. title es el nombre corto, description es la explicación detallada. Esto hace que tu API sea autodocumentada — otro developer lee /docs y entiende qué espera cada parámetro sin preguntarte.
Body() — Control del request body (con metadatos)
Ya usaste Body(...) en el Módulo 2 para indicar que un dict viene del body. Ahora vas a agregar metadatos que mejoran la documentación.
Ejemplo básico: Body con ejemplo y descripción
Agrega Body al import y usa la misma lista books con generate_id() del Módulo 2:
@app.post("/books", status_code=201)
def create_book(
book: dict = Body(
...,
description="Datos del libro a crear. Debe incluir title y author como mínimo.",
example={
"title": "El Aleph",
"author": "Jorge Luis Borges",
"year": 1949,
"genre": "Cuentos",
},
),
):
book["id"] = generate_id()
books.append(book)
return book
Dos parámetros clave:
examplehace que/docsmuestre ese JSON pre-llenado en el textarea cuando haces "Try it out". En lugar de un body vacío, el developer ve datos realistas que puede ejecutar directamente.descriptionaparece junto al campo del body en/docs. El developer sabe qué campos son esperados antes de probar.
curl -X POST http://127.0.0.1:8000/books \
-H "Content-Type: application/json" \
-d '{"title": "Pedro Páramo", "author": "Juan Rulfo", "year": 1955, "genre": "Novela"}'
Output esperado:
{"title": "Pedro Páramo", "author": "Juan Rulfo", "year": 1955, "genre": "Novela", "id": 4}
Combinando Path + Query + Body en un endpoint
Aquí es donde todo se conecta. Un endpoint PUT que actualiza un libro necesita tres fuentes de datos al mismo tiempo. Usando el mismo setup con Path, Query y Body importados:
@app.put("/books/{book_id}")
def update_book(
book_id: int = Path(..., ge=1, description="ID del libro a actualizar"),
notify: bool = Query(default=False, description="Enviar notificación del cambio"),
book_data: dict = Body(
...,
example={"title": "Título actualizado", "author": "Autor actualizado"},
),
):
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return {"error": "Book not found"}
book.update(book_data)
result = {"updated_book": book}
if notify:
result["notification"] = f"Book {book_id} was updated"
return result
¿Cómo distingue FastAPI de dónde viene cada dato?
La regla es clara y determinista:
| Fuente | Cómo lo identifica FastAPI | Ejemplo |
|---|---|---|
| Path parameter | El nombre aparece en la ruta ({book_id}) | /books/3 → book_id = 3 |
| Query parameter | Tipo simple (str, int, bool) que NO está en la ruta | ?notify=true → notify = True |
| Body | Marcado con Body(), o tipo complejo como un modelo Pydantic | JSON en el cuerpo del request |
No hay magia. Si el nombre del parámetro coincide con un {placeholder} en la ruta, es path. Si es un tipo simple sin {placeholder}, es query. Si tiene Body(), es body.
Prueba el endpoint combinado:
curl -X PUT "http://127.0.0.1:8000/books/1?notify=true" \
-H "Content-Type: application/json" \
-d '{"title": "Cien años de soledad (Edición especial)"}'
Output esperado:
{
"updated_book": {
"id": 1,
"title": "Cien años de soledad (Edición especial)",
"author": "Gabriel García Márquez",
"year": 1967,
"genre": "Realismo mágico"
},
"notification": "Book 1 was updated"
}
Sin ?notify=true:
curl -X PUT "http://127.0.0.1:8000/books/2" \
-H "Content-Type: application/json" \
-d '{"year": 1615}'
Output esperado — sin notificación:
{
"updated_book": {
"id": 2,
"title": "Don Quijote de la Mancha",
"author": "Miguel de Cervantes",
"year": 1615,
"genre": "Novela"
}
}
Validación de strings con pattern
Además de min_length y max_length, puedes validar strings con expresiones regulares:
@app.get("/books")
def list_books(
genre: str = Query(
default=None,
min_length=2,
max_length=30,
pattern="^[a-zA-ZáéíóúÁÉÍÓÚñÑ ]+$",
description="Solo letras y espacios",
),
):
return {"genre_filter": genre}
pattern acepta una expresión regular. En este caso, solo permite letras (incluyendo acentos) y espacios. Si el cliente envía genre=Sci-Fi!, FastAPI retorna 422 porque contiene caracteres no permitidos.
curl "http://127.0.0.1:8000/books?genre=Realismo%20mágico"
Output:
{"genre_filter": "Realismo mágico"}
curl "http://127.0.0.1:8000/books?genre=Sci-Fi!"
Output — rechazado por el pattern:
{
"detail": [
{
"type": "string_pattern_mismatch",
"loc": ["query", "genre"],
"msg": "String should match pattern '^[a-zA-ZáéíóúÁÉÍÓÚñÑ ]+$'"
}
]
}
Comparación: Type hints simples vs Path()/Query()/Body()
| Aspecto | Type hint simple | Path()/Query()/Body() |
|---|---|---|
| Validación de tipo | Sí (int, str, bool) | Sí |
| Valor por defecto | param: int = 10 | Query(default=10) |
| Descripción en /docs | No | description="..." |
| Constraints numéricos | No | ge, le, gt, lt |
| Constraints de string | No | min_length, max_length, pattern |
| Ejemplo en /docs | No | example={...} |
| Alias | No | alias="sort-by" |
| Deprecar parámetro | No | deprecated=True |
| Título para docs | No | title="..." |
| Cuándo usarlo | Prototipos, endpoints simples | APIs profesionales, endpoints públicos |
La recomendación: empieza con type hints simples cuando estás prototipando. Cuando el endpoint va a producción o lo consume otro equipo, agrega Path(), Query() y Body() con descripciones y constraints. No hay overhead de rendimiento — solo metadatos.
Conexión con Proyecto
Los constraints de Query() y Path() que aprendiste aquí aparecen directamente en el proyecto CRUD del Módulo 6. Cuando implementes GET /tasks?status=pending&limit=20, usarás Query(description=..., ge=..., le=...) para que la API se documente sola y rechace valores inválidos. Path(ge=1) protegerá todos los endpoints que reciben un task_id. En el Módulo 4 aprenderás Pydantic models, que hacen innecesario Body() para dicts — pero Path() y Query() los seguirás usando siempre.
Troubleshooting
Problema 1: El constraint no parece aplicarse
Causa: Usaste un type hint simple en lugar de Query() o Path().
Solución:
# ❌ ge no existe en type hints simples — esto da error de sintaxis
@app.get("/books")
def list_books(limit: int = 10, ge=1):
...
# ✅ ge va dentro de Query()
@app.get("/books")
def list_books(limit: int = Query(default=10, ge=1)):
...
ge, le, min_length, max_length y pattern solo funcionan dentro de Path(), Query() o Body(). No son parámetros de la función del endpoint.
Problema 2: TypeError al usar Query() sin default en parámetro opcional
Causa: Marcaste el parámetro como requerido (...) pero esperabas que fuera opcional.
Solución:
# ❌ Requerido — si no lo envías, error 422
@app.get("/books")
def list_books(genre: str = Query(..., description="Género")):
...
# ✅ Opcional — default=None
@app.get("/books")
def list_books(genre: str = Query(default=None, description="Género")):
...
Query(...) (con ...) significa "este query parameter es obligatorio." Query(default=None) lo hace opcional. Para query parameters, opcional es casi siempre lo correcto.
Problema 3: El alias no funciona — el parámetro siempre es None
Causa: Estás enviando el nombre de Python en la URL en lugar del alias.
Solución:
@app.get("/books")
def list_books(
sort_by: str = Query(default=None, alias="sort-by"),
):
return {"sort": sort_by}
# ❌ Usa el nombre Python — no funciona
curl "http://127.0.0.1:8000/books?sort_by=title"
# sort_by será None porque FastAPI busca "sort-by"
# ✅ Usa el alias definido
curl "http://127.0.0.1:8000/books?sort-by=title"
# sort_by será "title"
Cuando defines un alias, el cliente debe usar el alias en la URL. El nombre de Python (sort_by) es solo para tu código interno.
Problema 4: pattern rechaza caracteres válidos como acentos
Causa: Tu regex no incluye caracteres con acento ni la ñ.
Solución:
# ❌ Solo ASCII — rechaza "Realismo mágico"
genre: str = Query(default=None, pattern="^[a-zA-Z ]+$")
# ✅ Incluye acentos y ñ
genre: str = Query(default=None, pattern="^[a-zA-ZáéíóúÁÉÍÓÚñÑ ]+$")
Si tu API maneja datos en español, recuerda incluir los caracteres acentuados en los patterns.
Ejercicios
Ejercicio 1: Query parameters con constraints (Fácil)
Crea un endpoint GET /books que reciba skip (default 0, mínimo 0) y limit (default 5, mínimo 1, máximo 20). Ambos con descripciones. Retorna los libros desde skip hasta skip + limit.
Ver solución
Usando la misma lista books de 5 libros de la cápsula:
@app.get("/books")
def list_books(
skip: int = Query(default=0, ge=0, description="Número de libros a saltar"),
limit: int = Query(default=5, ge=1, le=20, description="Máximo de resultados (1-20)"),
):
return books[skip : skip + limit]
curl "http://127.0.0.1:8000/books?skip=2&limit=2"
# → [Rayuela, La casa de los espíritus]
curl "http://127.0.0.1:8000/books?limit=0"
# → Error 422: "Input should be greater than or equal to 1"
curl "http://127.0.0.1:8000/books?skip=-1"
# → Error 422: "Input should be greater than or equal to 0"
Ejercicio 2: Path con validación estricta (Fácil)
Crea un endpoint GET /books/{book_id} donde book_id debe ser un entero entre 1 y 999 (inclusive). Agrega descripción y título. Prueba con valores fuera de rango.
Ver solución
Usando la misma lista books de la cápsula:
@app.get("/books/{book_id}")
def get_book(
book_id: int = Path(
...,
ge=1,
le=999,
title="Book ID",
description="Identificador del libro (1-999)",
),
):
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return {"error": "Book not found"}
return book
curl http://127.0.0.1:8000/books/1
# → {"id": 1, "title": "Cien años de soledad", ...}
curl http://127.0.0.1:8000/books/0
# → Error 422: ge=1
curl http://127.0.0.1:8000/books/1000
# → Error 422: le=999
Ejercicio 3: Búsqueda con min_length y max_length (Medio)
Crea un endpoint GET /books/search que reciba un query parameter q (requerido, mínimo 3 caracteres, máximo 100) y retorne los libros cuyo título o autor contengan ese texto. Agrega descripción.
Ver solución
Usando la misma lista books de 5 libros:
@app.get("/books/search")
def search_books(
q: str = Query(
...,
min_length=3,
max_length=100,
description="Texto de búsqueda (3-100 caracteres). Busca en título y autor.",
),
):
query = q.lower()
results = [
b for b in books
if query in b["title"].lower() or query in b["author"].lower()
]
return {"query": q, "total_results": len(results), "books": results}
curl "http://127.0.0.1:8000/books/search?q=borges"
# → {"query": "borges", "total_results": 1, "books": [{...Ficciones...}]}
curl "http://127.0.0.1:8000/books/search?q=ab"
# → Error 422: min_length=3
curl "http://127.0.0.1:8000/books/search?q=soledad"
# → {"query": "soledad", "total_results": 1, "books": [{...Cien años...}]}
Ejercicio 4: PUT con Path + Query + Body completo (Medio)
Crea un endpoint PUT /books/{book_id} que:
book_id: Path, requerido, ge=1replace: Query, default False, descripción "Si true, reemplaza todos los campos"book_data: Body, con ejemplo embebido
Si replace es True, reemplaza el libro completo (conservando el id). Si es False, solo actualiza los campos enviados.
Ver solución
Usando la misma lista books de la cápsula:
@app.put("/books/{book_id}")
def update_book(
book_id: int = Path(..., ge=1, description="ID del libro a actualizar"),
replace: bool = Query(default=False, description="Si true, reemplaza todos los campos"),
book_data: dict = Body(
...,
example={"title": "Nuevo título", "author": "Nuevo autor", "year": 2024, "genre": "Ficción"},
),
):
book_index = next((i for i, b in enumerate(books) if b["id"] == book_id), None)
if book_index is None:
return {"error": "Book not found"}
if replace:
book_data["id"] = book_id
books[book_index] = book_data
else:
books[book_index].update(book_data)
return {"mode": "replace" if replace else "merge", "book": books[book_index]}
# Merge (default) — solo actualiza year
curl -X PUT "http://127.0.0.1:8000/books/1" \
-H "Content-Type: application/json" \
-d '{"year": 2024}'
# → {"mode": "merge", "book": {"id": 1, "title": "Cien años de soledad", ..., "year": 2024}}
# Replace — reemplaza todo excepto id
curl -X PUT "http://127.0.0.1:8000/books/2?replace=true" \
-H "Content-Type: application/json" \
-d '{"title": "Nuevo libro", "author": "Nuevo autor"}'
# → {"mode": "replace", "book": {"title": "Nuevo libro", "author": "Nuevo autor", "id": 2}}
# ID inválido
curl -X PUT "http://127.0.0.1:8000/books/0" \
-H "Content-Type: application/json" \
-d '{"title": "Test"}'
# → Error 422: ge=1
Resumen
Query()agrega descripciones, constraints y alias a query parameters — hace que/docssea autodocumentadoPath()agrega validación a path parameters —ge=1evita IDs negativos o cero antes de que tu código se ejecuteBody()agrega ejemplos y descripciones al request body — mejora la experiencia en/docs- Constraints numéricos:
ge(>=),gt(>),le(<=),lt(<) — FastAPI retorna 422 automáticamente si se violan - Constraints de string:
min_length,max_length,pattern— validación sin escribirifmanuales - FastAPI distingue parámetros por ubicación: path → de la URL, query → de
?key=value, body → del JSON aliasconecta nombres de URL (con guiones) con nombres Python (con guiones bajos)deprecated=Truemarca parámetros como obsoletos en la documentación- Empieza simple, agrega control después: type hints para prototipos,
Path()/Query()/Body()para producción
Próxima cápsula: Headers y Cookies — Aprenderás a leer headers HTTP y cookies con Header() y Cookie(), completando las cuatro fuentes de datos que FastAPI maneja en un request.
Recursos Adicionales
- FastAPI - Query Parameters and String Validations - Query() con constraints y metadatos en la documentación oficial
- FastAPI - Path Parameters and Numeric Validations - Path() con validaciones numéricas
- FastAPI - Body - Fields - Body() y Field() para metadatos del request body
- FastAPI - Body - Multiple Parameters - Combinar Path, Query y Body en un endpoint
- OpenAPI Specification - Parameter Object - Cómo los metadatos de Path()/Query() se traducen a OpenAPI
- FastAPI - Schema Extra/Example - Ejemplos embebidos en la documentación automática