Curso de Python / Crear una API JSON con Flask y validar entradas

Una API pequeña de Flask conecta peticiones HTTP con funciones de Python. Comprueba el objeto JSON, sus campos, tipos y rangos antes de almacenar datos, y utiliza estados HTTP para distinguir una creación correcta de una entrada inválida.

Preparar un servicio de desarrollo local

El ejemplo se ejecuta en el propio equipo. Sus gastos se guardan en una lista en memoria: desaparecen al cerrar el proceso y no se comparten entre procesos independientes. Un servicio público necesita persistencia, autenticación, autorización y un diseño de despliegue y concurrencia.

Instala Flask dentro del entorno virtual del proyecto:

python -m pip install flask

Guarda la aplicación como app.py, evitando llamarla flask.py para no ocultar el paquete. La introducción oficial de Flask explica la relación entre aplicaciones, rutas y funciones.

Guardar la aplicación completa

from flask import Flask, jsonify, request

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 4096
expenses = []

@app.errorhandler(413)
def too_large(error):
    return jsonify(error="request body too large"), 413

@app.get("/api/expenses")
def list_expenses():
    return jsonify(expenses)

@app.post("/api/expenses")
def add_expense():
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return jsonify(error="expected JSON object"), 400
    name, amount = data.get("name"), data.get("amount")
    if not isinstance(name, str) or not name.strip() or type(amount) is not int or amount < 0:
        return jsonify(error="invalid name or amount"), 400
    row = {"id": len(expenses) + 1, "name": name.strip(), "amount": amount}
    expenses.append(row)
    return jsonify(row), 201

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=False)

Ejecuta python app.py para iniciar el servidor en 127.0.0.1:5000. Si el puerto está ocupado, cambia puerto y dirección del cliente conjuntamente. GET /api/expenses devuelve la lista y POST /api/expenses crea un gasto.

La creación correcta devuelve 201; una entrada inválida devuelve 400. MAX_CONTENT_LENGTH limita el cuerpo a 4096 bytes y el manejador de 413 produce un error JSON. Sin ese manejador, una página de error predeterminada puede ser HTML. Este límite no sustituye todos los límites de recursos que necesitaría una aplicación real.

Ejercitar las rutas sin arrancar otro servidor

Guarda test_client.py junto a app.py. No necesita que la ejecución manual del servidor siga abierta: el cliente de pruebas llama al flujo de Flask dentro del proceso.

from app import app, expenses
expenses.clear()
with app.test_client() as client:
    created = client.post("/api/expenses", json={"name":"lunch", "amount":8000})
    assert created.status_code == 201
    for payload in [{"name":"x","amount":True}, {"name":"x","amount":-1}, {"name":" ","amount":1}, [1,2]]:
        response = client.post("/api/expenses", json=payload)
        assert response.status_code == 400 and response.is_json
    assert client.post("/api/expenses", data="{", content_type="application/json").status_code == 400
    too_large = client.post("/api/expenses", data="x"*5000, content_type="application/json")
    assert too_large.status_code == 413 and too_large.is_json
    rows = client.get("/api/expenses")
    assert rows.status_code == 200 and len(rows.get_json()) == 1
    print(created.status_code, rows.get_json()[0]["amount"])
print("validation and 413 checks passed")
python test_client.py

La salida esperada contiene 201 8000 y validation and 413 checks passed. Las comprobaciones cubren booleanos, negativos, nombres vacíos, listas, JSON mal formado y un cuerpo demasiado grande. Los rechazos no deben añadir filas; al final solo permanece el gasto válido.

Estas pruebas no verifican la red real, un proxy inverso ni el despliegue. La gestión de errores de Flask y la configuración de tamaño máximo describen las condiciones de esas respuestas. Un servidor o proxy puede rechazar una petición antes de que llegue a estos manejadores.

Definir validación y duración de los datos

El valor JSON true se convierte en bool. Como isinstance(True, int) es verdadero, se utiliza una comprobación exacta de tipo para excluirlo de los importes. Con silent=True, el JSON que no se interpreta correctamente produce None y entra en el rechazo definido por la aplicación.

La representación JSON no aporta almacenamiento persistente. Detén el servidor con Ctrl+C y comprueba que al reiniciar se vacía la lista. Los registros duraderos necesitan las reglas de almacenamiento y transacciones. Los identificadores derivados de la longitud de una lista sirven para esta demostración, pero no resuelven la sincronización de un servicio concurrente.

Mantén el servidor de desarrollo local y no expongas un depurador interactivo en un despliegue público. Para ampliar el programa, conserva primero las pruebas de comportamiento y sustituye después la lista por una capa de almacenamiento con responsabilidades explícitas.