¿Qué es Flask?
Flask es un framework web minimalista para Python. Te proporciona un objeto de aplicación, una forma de mapear URLs a funciones y un modelo de solicitud y respuesta. Todo lo demás —bases de datos, autenticación, formularios, tareas en segundo plano— es una decisión que tomas más adelante, generalmente añadiendo una extensión.
Esa simplicidad es precisamente su objetivo. Django tiene una opinión definida sobre cómo debe estructurarse un proyecto; Flask casi no tiene ninguna. Para una API pequeña, una herramienta interna o un primer proyecto web, esa libertad es una ventaja. Puedes leer el framework completo en una tarde y entender cada línea de tu propia aplicación.
Flask está construido sobre dos librerías consolidadas: Werkzeug para la infraestructura WSGI y Jinja2 para las plantillas. Ambas son maduras y están bien documentadas, y saber que están en la base explica la mayor parte del comportamiento de Flask.
La aplicación de Flask más pequeña
Una aplicación completa de Flask puede caber en unas pocas líneas.
# app.py
from flask import Flask
app = Flask(__name__)
@app.get("/")
def index():
return "Hello, world"
if __name__ == "__main__":
app.run(debug=True)
Flask(__name__) crea la aplicación y le indica dónde encontrar las plantillas y los archivos estáticos. El decorador vincula una URL a la función que tiene debajo. app.run() inicia el servidor de desarrollo. El guardián if __name__ == "__main__" evita que el servidor se inicie cuando el módulo es importado por una prueba o un servidor WSGI.
Enrutamiento con decoradores
El decorador @app.route es el corazón de Flask. Registra una regla de URL y la función que responde a ella.
@app.route("/posts")
def list_posts():
return "All posts"
@app.route("/posts/<int:post_id>")
def get_post(post_id):
return f"Post {post_id}"
Las reglas pueden contener partes variables escritas entre paréntesis angulares. Flask coincide con la URL, convierte el valor y lo pasa a la función como un argumento de palabra clave (keyword argument). Si ninguna regla coincide, Flask devuelve automáticamente un error 404.
Variables de URL y métodos HTTP
Los conversores hacen que las variables sean type-safe antes de que se ejecute tu código. Los más comunes son string (el predeterminado), int, float, path (que permite barras diagonales) y uuid.
@app.get("/users/<username>")
def profile(username):
return f"Profile for {username}"
@app.get("/files/<path:filepath>")
def serve_file(filepath):
return f"File at {filepath}"
Flask 2.0 añadió atajos para cada método. @app.get, @app.post, @app.put, @app.patch y @app.delete son más claros que pasar una lista methods, y una sola función puede responder a varios métodos.
@app.route("/posts", methods=["GET", "POST"])
def posts():
if request.method == "POST":
return create_post()
return list_posts()
El objeto request
Los datos entrantes residen en el objeto global request, el cual Flask puebla para la solicitud actual. Es un proxy, por lo que importarlo una sola vez al principio del módulo es seguro.
from flask import request
@app.get("/search")
def search():
query = request.args.get("q", "")
page = request.args.get("page", 1, type=int)
return {"query": query, "page": page}
@app.post("/posts")
def create_post():
payload = request.get_json()
title = payload["title"]
return {"title": title}, 201
request.args contiene los valores de la query-string, request.form contiene los posts de formularios, request.get_json() parsea un cuerpo JSON, request.files contiene las subidas de archivos y request.headers proporciona los headers en bruto. Debido a que estos son mapeos de valores múltiples, utiliza .get() y proporciona valores por defecto en lugar de acceder a los índices a ciegas.
Respuestas y JSON
Una vista puede devolver una cadena, un diccionario (que Flask serializa a JSON), una tupla de (body, status) o (body, status, headers), o un objeto Response completo.
from flask import jsonify, make_response
@app.post("/posts")
def create_post():
post = {"id": 1, "title": "Hello"}
return jsonify(post), 201
@app.get("/ping")
def ping():
response = make_response({"pong": True})
response.headers["Cache-Control"] = "no-store"
return response
jsonify es la forma explícita de devolver JSON: serializa los datos y establece el tipo de contenido. Devolver un diccionario también funciona, pero jsonify es más claro en cuanto a la intención y maneja más tipos de datos.
Plantillas con Jinja2
render_template carga un archivo de la carpeta templates/ y lo renderiza con los datos que le pases. Jinja2 escapa las variables por defecto, lo que protege contra XSS.
from flask import render_template
@app.get("/posts/<int:post_id>")
def get_post(post_id):
post = {"id": post_id, "title": "Hello", "body": "First post"}
return render_template("post.html", post=post)
<!-- templates/post.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{ post.title }}</title>
</head>
<body>
<h1>{{ post.title }}</h1>
<p>{{ post.body }}</p>
</body>
</html>
Jinja soporta {% extends %} para la herencia, {% include %} para fragmentos, {% for %} y {% if %} para la lógica, y filtros como {{ value|upper }}. Mantén las plantillas enfocadas en la presentación; si la lógica crece, muévela a la vista.
Archivos estáticos
Por defecto, Flask sirve cualquier archivo en la carpeta static/ en /static/.... Haz referencia a ella mediante url_for, que construye la URL a partir del nombre del endpoint.
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
En producción, deja que un reverse proxy o un CDN sirvan los archivos estáticos en lugar del proceso de Python. Es más rápido y libera a la aplicación para gestionar las solicitudes.
Blueprints
Un Blueprint es una colección de rutas y plantillas que pueden registrarse en una aplicación. Es la forma en que una aplicación de Flask de un solo módulo crece hasta convertirse en un proyecto sin volverse un caos.
# blog/views.py
from flask import Blueprint, jsonify
bp = Blueprint("blog", __name__, url_prefix="/blog")
@bp.get("/")
def index():
return jsonify(posts=[])
@bp.get("/<int:post_id>")
def detail(post_id):
return jsonify(id=post_id)
# app.py
from blog.views import bp as blog_bp
app.register_blueprint(blog_bp)
Los Blueprints tienen su propio prefijo de URL, plantillas y archivos estáticos, y un mismo blueprint puede registrarse varias veces con diferentes prefijos. También facilitan las pruebas y la revisión de código porque cada funcionalidad es autónoma.
La factoría de la aplicación
Una vez que empiezas a usar blueprints, el siguiente paso natural es crear una función factoría que construya la aplicación. Esta es la estructura estándar para cualquier proyecto que sea más que una simple demo.
# app.py
from flask import Flask
from blog.views import bp as blog_bp
def create_app(config_object="config.Config"):
app = Flask(__name__)
app.config.from_object(config_object)
app.register_blueprint(blog_bp)
return app
# wsgi.py
from app import create_app
app = create_app()
La factoría hace que las pruebas sean triviales: construyes una aplicación con una configuración de test, ejecutas el cliente y luego la descartas. También evita la importación de una aplicación global medio configurada, que es el origen de la mayoría de los problemas de importaciones circulares en Flask.
Extensiones
Dado que el núcleo es minimalista, el ecosistema proporciona el resto. Unas pocas extensiones cubren la mayoría de los proyectos:
- Flask-SQLAlchemy integra el ORM de SQLAlchemy y gestiona la sesión por solicitud.
- Flask-Migrate envuelve Alembic para que los cambios de esquema estén versionados.
- Flask-Login gestiona las sesiones de usuario y el proxy
current_user. - Flask-WTF añade formularios, protección CSRF y subida de archivos.
- Flask-CORS controla las solicitudes de origen cruzado para APIs.
# extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
# app.py
from extensions import db, migrate
def create_app():
app = Flask(__name__)
app.config.from_object("config.Config")
db.init_app(app)
migrate.init_app(app, db)
return app
El patrón init_app permite que una extensión se cree sin una aplicación y se vincule posteriormente, lo que mantiene la factoría limpia y permite que la extensión sea importable por los modelos.
Configuración
La configuración es un mapeo simple en app.config. Cárgala desde una clase, un archivo o el entorno, y mantén los secretos fuera del control de versiones.
# config.py
import os
class Config:
SECRET_KEY = os.environ["SECRET_KEY"]
SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
JSON_SORT_KEYS = False
app.config.from_object("config.Config")
app.config.from_prefixed_env()
from_prefixed_env() lee las variables de entorno de FLASK_*, lo cual es conveniente en contenedores. Utiliza diferentes clases o archivos por entorno, y nunca hagas commit del SECRET_KEY real; este se utiliza para firmar sesiones y tokens CSRF.
Manejo de errores
Registra manejadores de errores para devolver respuestas consistentes en toda la aplicación. Los manejadores pueden ser globales o estar limitados al alcance de un blueprint.
from flask import jsonify
from werkzeug.exceptions import HTTPException
@app.errorhandler(404)
def not_found(error):
return jsonify(error="not_found"), 404
@app.errorhandler(Exception)
def unhandled(error):
if isinstance(error, HTTPException):
return error
app.logger.exception(error)
return jsonify(error="internal_error"), 500
El manejador para HTTPException es importante: los errores integrados de Flask también son excepciones, y de lo contrario, un manejador genérico de Exception convertiría un 404 en un 500.
Pruebas
El cliente de pruebas de Flask realiza solicitudes en el mismo proceso, sin necesidad de un servidor ni de red.
import pytest
from app import create_app
@pytest.fixture
def client():
app = create_app("config.TestConfig")
with app.test_client() as client:
yield client
def test_health(client):
response = client.get("/health")
assert response.status_code == 200
assert response.get_json() == {"status": "ok"}
def test_missing_post(client):
response = client.get("/posts/999")
assert response.status_code == 404
La factoría y test_client() trabajan en conjunto: cada prueba recibe una instancia limpia de la aplicación, evitando así que el estado se filtre entre pruebas. Sobrescribe la configuración para las pruebas y utiliza una base de datos en memoria cuando añadas una.
WSGI y despliegue
Flask es una aplicación WSGI, lo que significa que cualquier servidor WSGI puede ejecutarla. El servidor de desarrollo es exclusivamente para fines de desarrollo.
pip install gunicorn
gunicorn --workers 4 --bind 0.0.0.0:8000 "app:create_app()"
En producción, ejecuta Gunicorn o uWSGI detrás de Nginx, termina el TLS en el proxy y sirve los archivos estáticos desde el proxy o un CDN. Configura DEBUG=False, establece el logging y lee los secretos desde el entorno. Contenerizar la aplicación hace que el despliegue sea portable entre distintos hosts.
Mejores prácticas
- Comienza con un único módulo, y luego migra a una factory y blueprints a medida que el proyecto crezca.
- Utiliza
url_forpara construir URLs en lugar de escribir las rutas a mano. - Mantén la configuración en clases o variables de entorno, nunca directamente en el código.
- Utiliza
jsonifyy códigos de estado explícitos para las respuestas de la API. - Valida cada dato de entrada antes de que llegue a tu lógica de negocio.
- Registra manejadores de errores para que los fallos devuelvan una estructura consistente.
- Utiliza el cliente de pruebas y una instancia limpia de la app por cada test.
- Nunca ejecutes el servidor de desarrollo en producción.
Errores comunes
- Dejar
debug=Trueactivado en producción, lo que expone un depurador interactivo. - Importar un
appglobal en todas partes y crear importaciones circulares. - Leer
request.jsonsin gestionar cuerpos mal formados o ausentes. - Escribir URLs fijas (hardcoding) en las plantillas en lugar de usar
url_for. - Construir un
app.pymonolítico con cientos de rutas. - Olvidar la protección CSRF al utilizar formularios.
- Almacenar secretos en
config.pyy hacer commit del archivo. - Asumir que Flask tiene un ORM o un sistema de autenticación integrado y terminar reinventando ambos de forma deficiente.
Próximos pasos
Flask es la forma más rápida de aprender qué hace realmente un framework web, ya que oculta muy pocos detalles. Cuando necesites un ORM, migraciones, autenticación y un panel de administración sin tener que ensamblarlos manualmente, lee la guía de Django. Si buscas validación automática y documentación de API basada en type hints, pásate a FastAPI. Para diseñar los endpoints que acabas de construir, la guía de REST APIs es el complemento ideal. Si decides añadir una base de datos, el backend roadmap cubre SQL, el lenguaje que se encuentra debajo del ORM.