Module 2: Path Operations

POST: Crear Recursos

Descripción de la cápsula

En la cápsula anterior implementaste endpoints GET para listar libros y obtener uno por ID. Todo eso fue lectura — tu API solo devuelve datos que ya existen. Ahora vas a agregar la capacidad de crear datos nuevos. POST es el verbo HTTP que dice "quiero agregar un recurso nuevo a tu colección." Y en esta cápsula vas a implementarlo desde cero.

La diferencia fundamental entre GET y POST es la dirección de los datos. En GET, los datos fluyen del servidor al cliente: tú pides, el servidor responde. En POST, los datos fluyen del cliente al servidor: el cliente envía un JSON con la información del nuevo recurso, y el servidor lo procesa, lo almacena, y confirma la creación. Este flujo introduce un concepto nuevo que no usaste con GET: el request body — los datos que viajan dentro del cuerpo de la petición HTTP.

FastAPI hace que recibir y procesar un request body sea sorprendentemente simple, pero necesitas decirle explícitamente de dónde leer los datos. En esta cápsula aprenderás cómo hacerlo usando Body(), cómo generar IDs automáticamente, cómo retornar el recurso creado con el status code correcto (201), y cómo probar todo desde /docs y con curl.


Request body: El concepto clave de POST

¿Qué es un request body?

Cuando haces GET, toda la información viaja en la URL:

GET /books/3        ← el "3" va en la URL
GET /books?genre=fiction  ← el filtro va en la URL

Cuando haces POST, los datos del nuevo recurso viajan en el cuerpo de la petición HTTP, separados de la URL:

POST /books
Content-Type: application/json

{
    "title": "Dune",
    "author": "Frank Herbert",
    "year": 1965,
    "genre": "Science Fiction"
}

El body es como un paquete que el cliente envía dentro del request. La URL dice a dónde va el paquete (/books), y el body contiene qué trae el paquete (los datos del libro nuevo).

¿Cómo lo recibe FastAPI?

FastAPI necesita saber de dónde leer cada parámetro de tu función. Con path parameters es obvio — vienen de la URL. Pero con un dict, FastAPI no puede adivinar si es un query parameter o un body. Necesitas decírselo explícitamente.

Para eso existe Body():

from fastapi import FastAPI, Body

app = FastAPI()

@app.post("/books")
def create_book(book: dict = Body(...)):
    return book

El Body(...) le dice a FastAPI: "lee este parámetro del cuerpo del request, y es obligatorio." Los tres puntos (...) son la forma de Python de decir "este valor es requerido" (es el objeto Ellipsis de Python).

¿Por qué no basta con book: dict?

Intenta esto:

# ❌ Esto NO funciona como esperas
@app.post("/books")
def create_book(book: dict):
    return book

Si escribes solo book: dict sin Body(), FastAPI no sabe si debe buscar ese dato en la URL, en query parameters, o en el body. Con tipos simples como str o int, FastAPI asume que son query parameters. Con dict sin indicación, el comportamiento es impredecible. La regla es clara: si quieres leer del body sin usar un modelo Pydantic, usa Body().

En el Módulo 4 aprenderás Pydantic models — con ellos FastAPI sabe automáticamente que el dato viene del body, sin necesidad de Body(). Pero por ahora, Body() es tu herramienta.


Tu primer endpoint POST

El código base

Tu app/main.py de la cápsula anterior tiene la lista books con 3 libros y endpoints GET para listar y obtener por ID. Ahora agrega Body al import y el endpoint POST al final del archivo:

from fastapi import FastAPI, Body  # ← Agrega Body al import

# ... (books list y endpoints GET se mantienen igual)

@app.post("/books")
def create_book(book: dict = Body(...)):
    books.append(book)
    return book

Guarda, deja que uvicorn recargue, y prueba:

curl -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "El Aleph", "author": "Jorge Luis Borges", "year": 1949, "genre": "Cuentos"}'

Output esperado:

{"title": "El Aleph", "author": "Jorge Luis Borges", "year": 1949, "genre": "Cuentos"}

Funciona... pero tiene problemas. ¿Dónde está el id? ¿Y el status code? Vamos paso a paso.


Auto-generar IDs

Cada recurso necesita un identificador único. En una base de datos real, el motor genera el ID. Como estás usando una lista en memoria, necesitas generarlo tú.

Estrategia simple: max(id) + 1

Agrega esta función y modifica create_book:

def generate_id():
    if not books:
        return 1
    return max(book["id"] for book in books) + 1


@app.post("/books")
def create_book(book: dict = Body(...)):
    book["id"] = generate_id()
    books.append(book)
    return book

generate_id() busca el ID más alto en la lista y suma 1. Si la lista está vacía, empieza en 1. No es la solución más robusta — en producción usarías UUIDs o auto-increment de base de datos — pero para datos en memoria es suficiente.

Prueba crear dos libros seguidos:

curl -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "El Aleph", "author": "Jorge Luis Borges", "year": 1949, "genre": "Cuentos"}'
{"title": "El Aleph", "author": "Jorge Luis Borges", "year": 1949, "genre": "Cuentos", "id": 4}
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"}'
{"title": "Pedro Páramo", "author": "Juan Rulfo", "year": 1955, "genre": "Novela", "id": 5}

Los IDs se generan secuencialmente: 4, 5, 6... Verifica que se agregaron listando todos:

curl http://127.0.0.1:8000/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", "author": "Miguel de Cervantes", "year": 1605, "genre": "Novela"},
  {"id": 3, "title": "Rayuela", "author": "Julio Cortázar", "year": 1963, "genre": "Novela Experimental"},
  {"id": 4, "title": "El Aleph", "author": "Jorge Luis Borges", "year": 1949, "genre": "Cuentos"},
  {"id": 5, "title": "Pedro Páramo", "author": "Juan Rulfo", "year": 1955, "genre": "Novela"}
]

Status code 201: Comunicar que se creó algo

El problema con status 200

Ahora mismo, tu endpoint POST retorna status code 200 OK. Funciona, pero no es correcto semánticamente. El código 200 significa "todo salió bien", mientras que 201 Created dice específicamente "se creó un recurso nuevo." La diferencia importa porque:

  • Los clientes (frontend, mobile apps) usan el status code para decidir qué hacer
  • Los logs y sistemas de monitoreo interpretan los códigos
  • Las herramientas de testing validan status codes esperados

Agregar status_code al decorador

FastAPI hace esto trivial — agrega status_code=201 al decorador:

@app.post("/books", status_code=201)
def create_book(book: dict = Body(...)):
    book["id"] = generate_id()
    books.append(book)
    return book

Eso es todo. Un parámetro. Ahora cada vez que este endpoint se ejecute exitosamente, retornará status 201 en lugar de 200.

Verificar el status code

Con curl -v (verbose) puedes ver los headers de respuesta, incluyendo el status code:

curl -v -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Ficciones", "author": "Jorge Luis Borges", "year": 1944, "genre": "Cuentos"}'

En el output verás (entre mucha más información):

< HTTP/1.1 201 Created
< content-type: application/json

El 201 Created confirma que FastAPI está usando el status code correcto.

Alternativa: usar la constante de status

FastAPI también ofrece un módulo status con todas las constantes HTTP. Es más legible que recordar números:

from fastapi import FastAPI, Body
from starlette.status import HTTP_201_CREATED

@app.post("/books", status_code=HTTP_201_CREATED)
def create_book(book: dict = Body(...)):
    book["id"] = generate_id()
    books.append(book)
    return book

Ambas formas (status_code=201 y status_code=HTTP_201_CREATED) son equivalentes. Usa la que prefieras — en este módulo usaremos el número directo por simplicidad.


El Response model: Qué retornar después de crear

¿Qué debe retornar un POST?

Cuando creas un recurso, la convención REST es retornar el recurso completo recién creado, incluyendo los campos que el servidor generó (como el id). Esto le permite al cliente trabajar con el recurso sin hacer un GET adicional.

EstrategiaRetorno¿Cuándo?
Recurso completo{"id": 4, "title": "...", ...}Estándar REST — lo más común
Solo el ID{"id": 4}Cuando el recurso es muy grande
Mensaje de confirmación{"message": "Created"}Poco común, pierde información
Nada (204)(vacío)Raro para POST, más común en DELETE

Tu endpoint ya hace lo correcto — retorna el libro completo con su ID asignado:

@app.post("/books", status_code=201)
def create_book(book: dict = Body(...)):
    book["id"] = generate_id()
    books.append(book)
    return book  # ← Retorna el recurso creado con ID

El cliente envía:

{"title": "Dune", "author": "Frank Herbert", "year": 1965, "genre": "Science Fiction"}

El servidor retorna:

{"title": "Dune", "author": "Frank Herbert", "year": 1965, "genre": "Science Fiction", "id": 4}

El campo id fue añadido por el servidor. El cliente ahora sabe que el libro se creó con id: 4 y puede usarlo para futuras operaciones (GET /books/4, PUT /books/4, DELETE /books/4).


Checkpoint: Código completo hasta aquí

Tu app/main.py ahora tiene: imports de FastAPI y Body, la lista books, generate_id(), los endpoints GET (list y detail), y el POST con status 201. Ejecuta uvicorn app.main:app --reload y verifica que todo funciona antes de continuar.


Probar POST en /docs (Swagger UI)

La diferencia con GET

Probar un GET en /docs es simple: haces clic en "Try it out", llenas los parámetros si los hay, y ejecutas. POST es diferente porque necesitas enviar un body — un bloque de JSON con los datos del recurso.

Paso a paso en /docs

  1. Abre http://127.0.0.1:8000/docs en tu navegador
  2. Busca el endpoint POST /books — lo verás en color verde (los POST son verdes, los GET azules)
  3. Haz clic en el endpoint para expandirlo
  4. Haz clic en "Try it out"
  5. Verás un textarea con un JSON de ejemplo. Reemplázalo con:
{
  "title": "La Casa de los Espíritus",
  "author": "Isabel Allende",
  "year": 1982,
  "genre": "Realismo Mágico"
}
  1. Haz clic en "Execute"

Lo que verás en la respuesta

Swagger UI te muestra tres cosas importantes:

Request URL:

http://127.0.0.1:8000/books

Response code:

201

Response body:

{
  "title": "La Casa de los Espíritus",
  "author": "Isabel Allende",
  "year": 1982,
  "genre": "Realismo Mágico",
  "id": 4
}

Fíjate: el status code dice 201 (no 200), y el response incluye el id que el servidor generó. Después de crear, ve al endpoint GET /books, ejecútalo, y verás el nuevo libro en la lista.

¿Por qué /docs es valioso para POST?

Con GET puedes probar directamente en el navegador (basta escribir la URL). Con POST no — el navegador solo hace GET cuando escribes una URL. Para POST necesitas una herramienta que te permita enviar un body. Swagger UI te da eso gratis, con una interfaz visual donde escribes el JSON y ves la respuesta. Es más rápido que escribir curl para cada prueba durante el desarrollo.


Probar POST con curl

El comando básico

curl por defecto hace GET. Para POST necesitas tres flags adicionales:

curl -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Ficciones", "author": "Jorge Luis Borges", "year": 1944, "genre": "Cuentos"}'

Desglose de cada flag:

FlagSignificado
-X POSTUsa el método HTTP POST (en lugar del GET por defecto)
-H "Content-Type: application/json"Header que dice: "el body que envío es JSON"
-d '...'El body (data) del request — tu JSON con los datos del libro

Ver headers y respuesta formateada

Agrega -v (verbose) para ver el status code en los headers:

curl -v -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "El Túnel", "author": "Ernesto Sabato", "year": 1948, "genre": "Novela"}'
> POST /books HTTP/1.1          ← lo que tú envías
> Content-Type: application/json
< HTTP/1.1 201 Created          ← lo que el servidor responde
{"title":"El Túnel","author":"Ernesto Sabato","year":1948,"genre":"Novela","id":4}

Para respuestas formateadas, usa -s (silent) y python -m json.tool:

curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Aura", "author": "Carlos Fuentes", "year": 1962, "genre": "Novela Corta"}' \
  | python -m json.tool

Flujo completo: Crear y verificar

Un patrón útil durante desarrollo:

# 1. Crear
curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Aura", "author": "Carlos Fuentes", "year": 1962, "genre": "Novela Corta"}'

# 2. Listar todos (verificar que aparece)
curl -s http://127.0.0.1:8000/books | python -m json.tool

# 3. Obtener por ID (verificar acceso directo)
curl -s http://127.0.0.1:8000/books/4 | python -m json.tool

Comparación: GET vs POST en FastAPI

AspectoGETPOST
Decorador@app.get("/books")@app.post("/books")
Datos de entradaPath/query params (URL)Request body (JSON)
PropósitoLeer recursosCrear recursos
Status code correcto200 OK201 Created
IdempotenteSí (repetir no cambia nada)No (repetir crea duplicados)
Probar en navegadorSí (escribir URL)No (necesitas /docs o curl)
curlcurl URLcurl -X POST -H ... -d ... URL

Un punto importante: GET y POST usan la misma ruta (/books) pero hacen cosas diferentes. Esto es convención REST — la URL identifica el recurso (/books = colección de libros), y el verbo HTTP indica la acción (GET = leer, POST = crear). FastAPI distingue entre ellas por el decorador.

Status 200 vs 201: Si usas 200 para POST, el endpoint funciona. Pero pierdes semántica — el cliente no sabe si "200" significa "leí datos" o "creé algo nuevo." Con 201, el significado es inequívoco.


Conexión con Proyecto

El POST que implementaste aquí es el mismo patrón del proyecto final (To-Do List API, Módulo 6). El flujo es idéntico: recibir datos → asignar ID → almacenar → retornar. Lo que cambia son las herramientas — en el Módulo 4, un modelo Pydantic reemplazará tanto a dict como a Body(), y FastAPI sabrá automáticamente que el dato viene del body. Primero aprendes el patrón, después las herramientas profesionales.


Troubleshooting

Problema 1: 422 Unprocessable Entity al hacer POST

Causa: El body no es JSON válido, o no enviaste el header Content-Type: application/json.

Solución:

# ❌ Sin header Content-Type — FastAPI no sabe que es JSON
curl -X POST http://127.0.0.1:8000/books \
  -d '{"title": "Dune"}'

# ✅ Con header Content-Type
curl -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Dune"}'

Si usas /docs, Swagger UI envía el header automáticamente. Este problema es específico de curl o clientes HTTP manuales.

Problema 2: El body llega vacío o como None

Causa: Usaste book: dict sin Body(...).

Solución:

# ❌ FastAPI no sabe leer del body
@app.post("/books")
def create_book(book: dict):
    ...

# ✅ Body() le indica a FastAPI de dónde leer
@app.post("/books")
def create_book(book: dict = Body(...)):
    ...

Recuerda: sin Body() ni un modelo Pydantic, FastAPI no sabe que dict debe leerse del request body.

Problema 3: Los IDs se reinician al recargar el servidor

Causa: Los datos viven en memoria. Al reiniciar uvicorn, la lista vuelve a su estado inicial y todo lo que creaste con POST se pierde.

Solución: Esto es comportamiento esperado con almacenamiento en memoria. No es un bug — para persistencia real necesitas una base de datos (guías posteriores del path).

Problema 4: JSON con comillas simples da error

Causa: JSON requiere comillas dobles. Las comillas simples no son JSON válido.

Solución:

# ❌ Comillas simples — no es JSON válido
curl -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d "{'title': 'Dune'}"

# ✅ Comillas dobles — JSON correcto
curl -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Dune"}'

En la terminal, envuelve el JSON con comillas simples por fuera ('...') y usa comillas dobles dentro ("key": "value").

Problema 5: 405 Method Not Allowed

Causa: Estás usando el verbo HTTP incorrecto (ej: GET a una ruta que solo tiene POST).

Solución: Verifica en /docs qué verbos soporta cada ruta. Usa -X POST explícitamente en curl para endpoints POST.


Ejercicios

Ejercicio 1: POST básico con conteo (Fácil)

Modifica create_book para que la respuesta incluya un campo total_books con el total de libros después de agregar el nuevo. Si hay 3 libros y creas uno, la respuesta debe incluir "total_books": 4.

Ver solución
@app.post("/books", status_code=201)
def create_book(book: dict = Body(...)):
    book["id"] = generate_id()
    books.append(book)
    return {
        "book": book,
        "total_books": len(books)
    }
curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Ficciones", "author": "Jorge Luis Borges", "year": 1944, "genre": "Cuentos"}' \
  | python -m json.tool
{
    "book": {
        "title": "Ficciones",
        "author": "Jorge Luis Borges",
        "year": 1944,
        "genre": "Cuentos",
        "id": 4
    },
    "total_books": 4
}

Ejercicio 2: Verificar campos requeridos (Fácil)

Antes de agregar el libro, verifica que el dict tenga al menos title y author. Si falta alguno, retorna un mensaje de error. No cambies el status code — el error handling profesional viene en Módulo 5.

Ver solución
@app.post("/books", status_code=201)
def create_book(book: dict = Body(...)):
    required_fields = ["title", "author"]
    missing = [field for field in required_fields if field not in book]

    if missing:
        return {"error": f"Missing required fields: {', '.join(missing)}"}

    book["id"] = generate_id()
    books.append(book)
    return book
curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Ficciones"}' | python -m json.tool
# {"error": "Missing required fields: author"}

curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Ficciones", "author": "Jorge Luis Borges"}' | python -m json.tool
# {"title": "Ficciones", "author": "Jorge Luis Borges", "id": 4}

El status code sigue siendo 201 incluso en error — en Módulo 5 aprenderás a retornar 400 con HTTPException.

Ejercicio 3: Evitar títulos duplicados (Medio)

Antes de crear un libro, verifica que no exista otro con el mismo título (comparación case-insensitive). Si ya existe, retorna un error con el ID del libro existente.

Ver solución

Agrega una función helper y modifica create_book:

def find_by_title(title):
    for book in books:
        if book["title"].lower() == title.lower():
            return book
    return None


@app.post("/books", status_code=201)
def create_book(book: dict = Body(...)):
    if "title" in book:
        existing = find_by_title(book["title"])
        if existing:
            return {
                "error": "A book with this title already exists",
                "existing_book_id": existing["id"]
            }

    book["id"] = generate_id()
    books.append(book)
    return book
curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "cien años de soledad", "author": "Otro Autor"}' | python -m json.tool
# {"error": "A book with this title already exists", "existing_book_id": 1}

curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "El Aleph", "author": "Jorge Luis Borges", "year": 1949, "genre": "Cuentos"}' | python -m json.tool
# {"title": "El Aleph", "author": "Jorge Luis Borges", "year": 1949, "genre": "Cuentos", "id": 4}

find_by_title() compara títulos en minúsculas (.lower()). No es perfecto — dos libros podrían tener el mismo título de autores diferentes — pero demuestra el patrón de verificación antes de crear.

Ejercicio 4: Endpoint POST para otra colección (Medio)

Agrega una colección authors (lista de dicts con id, name, country, birth_year) con 2 autores iniciales. Implementa GET /authors y POST /authors con el mismo patrón de libros.

Ver solución

Agrega esto a tu app/main.py:

authors = [
    {"id": 1, "name": "Gabriel García Márquez", "country": "Colombia", "birth_year": 1927},
    {"id": 2, "name": "Julio Cortázar", "country": "Argentina", "birth_year": 1914},
]


def generate_author_id():
    if not authors:
        return 1
    return max(author["id"] for author in authors) + 1


@app.get("/authors")
def get_authors():
    return authors


@app.post("/authors", status_code=201)
def create_author(author: dict = Body(...)):
    author["id"] = generate_author_id()
    authors.append(author)
    return author
curl -s -X POST http://127.0.0.1:8000/authors \
  -H "Content-Type: application/json" \
  -d '{"name": "Isabel Allende", "country": "Chile", "birth_year": 1942}' | python -m json.tool
# {"name": "Isabel Allende", "country": "Chile", "birth_year": 1942, "id": 3}

El patrón es idéntico a libros: lista en memoria, función de ID, GET para listar, POST para crear. Esta repetición es exactamente lo que frameworks y ORMs ayudan a abstraer en proyectos reales.

Ejercicio 5: POST con valores por defecto (Difícil)

Modifica create_book para que si el cliente no envía genre, se asigne "Sin clasificar". Si no envía year, se asigne el año actual. Necesitarás from datetime import datetime.

Ver solución
from datetime import datetime
from fastapi import FastAPI, Body

# ... (books list y generate_id igual que antes)

@app.post("/books", status_code=201)
def create_book(book: dict = Body(...)):
    book["id"] = generate_id()

    if "genre" not in book:
        book["genre"] = "Sin clasificar"

    if "year" not in book:
        book["year"] = datetime.now().year

    books.append(book)
    return book
curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Libro Misterioso", "author": "Autor Desconocido"}' | python -m json.tool
# {"title": "Libro Misterioso", "author": "Autor Desconocido", "id": 4, "genre": "Sin clasificar", "year": 2026}

curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Novela Reciente", "author": "Escritor Nuevo", "genre": "Thriller"}' | python -m json.tool
# {"title": "Novela Reciente", "author": "Escritor Nuevo", "genre": "Thriller", "id": 5, "year": 2026}

En Módulo 4, Pydantic hará esto con Field(default=...) de forma más elegante, pero el concepto es el mismo: el servidor completa datos que el cliente no proporcionó.

Ejercicio 6: Crear múltiples libros en un request (Difícil)

Crea un endpoint POST /books/batch que reciba una lista de libros y los agregue todos. Retorna la lista de libros creados (con IDs) y un conteo.

Ver solución
@app.post("/books/batch", status_code=201)
def create_books_batch(new_books: list = Body(...)):
    created = []
    for book in new_books:
        book["id"] = generate_id()
        books.append(book)
        created.append(book)

    return {
        "created": created,
        "count": len(created)
    }
curl -s -X POST http://127.0.0.1:8000/books/batch \
  -H "Content-Type: application/json" \
  -d '[
    {"title": "El Aleph", "author": "Jorge Luis Borges", "year": 1949, "genre": "Cuentos"},
    {"title": "Pedro Páramo", "author": "Juan Rulfo", "year": 1955, "genre": "Novela"},
    {"title": "La Ciudad y los Perros", "author": "Mario Vargas Llosa", "year": 1963, "genre": "Novela"}
  ]' | python -m json.tool
{
    "created": [
        {"title": "El Aleph", "author": "Jorge Luis Borges", "year": 1949, "genre": "Cuentos", "id": 4},
        {"title": "Pedro Páramo", "author": "Juan Rulfo", "year": 1955, "genre": "Novela", "id": 5},
        {"title": "La Ciudad y los Perros", "author": "Mario Vargas Llosa", "year": 1963, "genre": "Novela", "id": 6}
    ],
    "count": 3
}

El body puede ser una lista en lugar de un dict — FastAPI lo acepta con list = Body(...). La ruta /books/batch evita ambigüedad con /books. En APIs reales, las operaciones batch son comunes para imports masivos.


Resumen

  • POST es el verbo HTTP para crear recursos nuevos
  • FastAPI necesita Body(...) para saber que un dict viene del request body (sin Pydantic)
  • @app.post("/books", status_code=201) define un endpoint POST con status code 201 Created
  • generate_id() calcula el siguiente ID disponible a partir del máximo existente
  • La convención REST es retornar el recurso completo creado (con ID) en la respuesta
  • En /docs, POST se prueba escribiendo JSON en el textarea y ejecutando
  • Con curl, necesitas -X POST, -H "Content-Type: application/json", y -d '{...}'
  • La misma ruta (/books) puede tener GET y POST — FastAPI los distingue por el verbo
  • Status 201 comunica "se creó algo nuevo", status 200 comunica "operación exitosa genérica"
  • Los datos en memoria se pierden al reiniciar — esto es esperado sin base de datos

Próxima cápsula: PUT, PATCH y DELETE — Completarás el CRUD implementando actualización completa, actualización parcial, y eliminación de recursos.


Recursos Adicionales

  1. FastAPI - Request Body - Cómo FastAPI maneja request bodies con JSON
  2. FastAPI - Response Status Code - Configurar status codes en endpoints
  3. FastAPI - Body - Fields - Opciones avanzadas de Body()
  4. HTTP POST Method - MDN - Especificación completa del método POST
  5. HTTP Status 201 - MDN - Significado y uso del status 201 Created
  6. curl Manual - POST - Guía oficial de curl para requests POST