Cuando empiezas un proyecto web en Python, una de las primeras decisiones que debes tomar es qué framework usar. Django, Flask y FastAPI son los tres más populares del ecosistema, pero tienen filosofías muy distintas: Django viene con todo incluido, Flask es minimalista y te deja elegir cada pieza, y FastAPI está diseñado específicamente para construir APIs modernas con alto rendimiento. Elegir mal puede costar semanas de trabajo más adelante.
En esta guía aprenderás qué hace cada framework, cuándo conviene cada uno y cómo se comparan en la práctica con ejemplos reales.
Flask: el microframework minimalista
Flask apareció en 2010 y se define a sí mismo como un microframework: viene con lo mínimo imprescindible (enrutamiento, gestión de peticiones y respuestas, templates con Jinja2) y te deja a ti elegir cómo hacer todo lo demás. Sin ORM, sin sistema de autenticación, sin panel de administración: añades lo que necesitas.
# pip install flask
from flask import Flask, request, jsonify
app = Flask(__name__)
# Base de datos simulada
usuarios = {
1: {"nombre": "Ana García", "email": "ana@mail.com"},
2: {"nombre": "Carlos López", "email": "carlos@mail.com"},
}
@app.route("/usuarios", methods=["GET"])
def listar_usuarios():
return jsonify(list(usuarios.values()))
@app.route("/usuarios/", methods=["GET"])
def obtener_usuario(id):
usuario = usuarios.get(id)
if not usuario:
return jsonify({"error": "Usuario no encontrado"}), 404
return jsonify(usuario)
@app.route("/usuarios", methods=["POST"])
def crear_usuario():
datos = request.get_json()
if not datos or not datos.get("nombre") or not datos.get("email"):
return jsonify({"error": "nombre y email son obligatorios"}), 400
nuevo_id = max(usuarios.keys()) + 1
usuarios[nuevo_id] = {"nombre": datos["nombre"], "email": datos["email"]}
return jsonify({"id": nuevo_id, **usuarios[nuevo_id]}), 201
if __name__ == "__main__":
app.run(debug=True)
# Ejecutar:
python app.py
# * Running on http://127.0.0.1:5000
# Flask es explícito: request, jsonify, status codes... todo lo controlas tú
El ecosistema de extensiones de Flask
# Flask no incluye ORM ni autenticación,
# pero hay extensiones para casi todo:
pip install flask-sqlalchemy # ORM con SQLAlchemy
pip install flask-migrate # migraciones de base de datos
pip install flask-login # gestión de sesiones de usuario
pip install flask-jwt-extended # autenticación JWT
pip install flask-cors # cabeceras CORS
pip install flask-limiter # rate limiting
pip install marshmallow # serialización y validación
# Estructura típica de un proyecto Flask mediano:
mi-proyecto/
├── app/
│ ├── __init__.py # crear la app Flask
│ ├── modelos.py # modelos de SQLAlchemy
│ ├── rutas/
│ │ ├── usuarios.py
│ │ └── pedidos.py
│ └── config.py
├── migrations/
├── tests/
└── run.py
Cuándo elegir Flask:
- Proyectos pequeños o medianos donde quieres control total sobre cada pieza.
- APIs REST sencillas que no necesitan autogeneración de documentación.
- Proyectos donde el stack ya está decidido y solo necesitas el enrutamiento.
- Cuando aprendes Python web: Flask enseña los fundamentos sin magia oculta.
- Microservicios pequeños con lógica muy específica.
Django: el framework con baterías incluidas
Django apareció en 2005 con una filosofía opuesta a Flask: viene con todo lo que necesitas para construir una aplicación web completa. ORM potente, sistema de migraciones, panel de administración automático, sistema de autenticación, gestión de archivos estáticos, templates, formularios, protección CSRF, internacionalización... todo incluido y funcionando de forma integrada desde el primer día.
# pip install django djangorestframework
# Crear proyecto y aplicación
django-admin startproject mi_tienda
cd mi_tienda
python manage.py startapp usuarios
# usuarios/models.py
from django.db import models
class Usuario(models.Model):
nombre = models.CharField(max_length=100)
email = models.EmailField(unique=True)
activo = models.BooleanField(default=True)
creado_en = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["nombre"]
def __str__(self):
return self.nombre
# Crear y aplicar migraciones
python manage.py makemigrations
python manage.py migrate
# Panel de administración automático: registrar el modelo
# usuarios/admin.py
from django.contrib import admin
from .models import Usuario
@admin.register(Usuario)
class UsuarioAdmin(admin.ModelAdmin):
list_display = ["nombre", "email", "activo", "creado_en"]
list_filter = ["activo"]
search_fields = ["nombre", "email"]
# Ahora en /admin/ tienes un panel completo para gestionar usuarios:
# crear, editar, filtrar, buscar, exportar... todo sin escribir ni una línea de frontend
# Django REST Framework: API REST sobre Django
# usuarios/serializers.py
from rest_framework import serializers
from .models import Usuario
class UsuarioSerializer(serializers.ModelSerializer):
class Meta:
model = Usuario
fields = ["id", "nombre", "email", "activo", "creado_en"]
read_only_fields = ["id", "creado_en"]
# usuarios/views.py
from rest_framework import viewsets, permissions, filters
from .models import Usuario
from .serializers import UsuarioSerializer
class UsuarioViewSet(viewsets.ModelViewSet):
queryset = Usuario.objects.all()
serializer_class = UsuarioSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ["nombre", "email"]
ordering_fields = ["nombre", "creado_en"]
ordering = ["nombre"]
# Una sola clase genera automáticamente:
# GET /usuarios/ → listar con búsqueda y paginación
# POST /usuarios/ → crear
# GET /usuarios/{id}/ → obtener uno
# PUT /usuarios/{id}/ → reemplazar
# PATCH /usuarios/{id}/ → actualizar parcialmente
# DELETE /usuarios/{id}/ → eliminar
# usuarios/urls.py
from rest_framework.routers import DefaultRouter
from .views import UsuarioViewSet
router = DefaultRouter()
router.register("usuarios", UsuarioViewSet)
urlpatterns = router.urls
# El ORM de Django: consultas expresivas sin SQL
from usuarios.models import Usuario
from django.db.models import Q
# Consultas básicas
todos = Usuario.objects.all()
activos = Usuario.objects.filter(activo=True)
uno = Usuario.objects.get(id=1)
# Filtros avanzados
recientes = Usuario.objects.filter(
creado_en__gte=datetime(2026, 1, 1)
).order_by("-creado_en")[:10]
# Búsqueda con OR
resultado = Usuario.objects.filter(
Q(nombre__icontains="garcia") | Q(email__icontains="garcia")
)
# Agregaciones
from django.db.models import Count, Avg
stats = Usuario.objects.aggregate(
total=Count("id"),
activos=Count("id", filter=Q(activo=True))
)
# Relacionar modelos
class Pedido(models.Model):
usuario = models.ForeignKey(Usuario, on_delete=models.CASCADE, related_name="pedidos")
total = models.DecimalField(max_digits=10, decimal_places=2)
creado_en = models.DateTimeField(auto_now_add=True)
# Consultas con relaciones (sin N+1 con select_related y prefetch_related)
pedidos = Pedido.objects.select_related("usuario").filter(total__gt=100)
usuarios_con_pedidos = Usuario.objects.prefetch_related("pedidos").filter(activo=True)
Cuándo elegir Django:
- Aplicaciones web completas con frontend basado en templates.
- Proyectos que se benefician del panel de administración (CMS, backoffice, intranets).
- Equipos que quieren convenciones fuertes y no reinventar la rueda.
- Proyectos con autenticación compleja, permisos por modelo y roles.
- Cuando la velocidad de desarrollo inicial importa más que la flexibilidad.
- Aplicaciones con modelos de datos complejos y muchas relaciones.
FastAPI: APIs modernas con tipado y rendimiento
FastAPI apareció en 2018 y desde entonces ha crecido a una velocidad impresionante. Está diseñado específicamente para construir APIs REST y GraphQL con Python moderno: aprovecha las anotaciones de tipo de Python 3.6+ para validar datos automáticamente, genera documentación interactiva sin configuración adicional y es uno de los frameworks más rápidos del ecosistema Python gracias a su base asíncrona.
# pip install fastapi uvicorn[standard] pydantic
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional
app = FastAPI(
title="API de Usuarios",
description="Gestión de usuarios de la plataforma",
version="1.0.0"
)
# Pydantic: modelos de datos con validación automática
class UsuarioBase(BaseModel):
nombre: str
email: EmailStr # valida que sea un email válido
class UsuarioCrear(UsuarioBase):
pass # los mismos campos para crear
class UsuarioRespuesta(UsuarioBase):
id: int
activo: bool
creado_en: datetime
class Config:
from_attributes = True
# Base de datos simulada
usuarios_db: dict[int, dict] = {
1: {"id": 1, "nombre": "Ana García", "email": "ana@mail.com", "activo": True, "creado_en": datetime.now()},
2: {"id": 2, "nombre": "Carlos López", "email": "carlos@mail.com", "activo": False, "creado_en": datetime.now()},
}
@app.get("/usuarios", response_model=list[UsuarioRespuesta])
def listar_usuarios(
activo: Optional[bool] = None,
pagina: int = Query(1, ge=1),
limite: int = Query(10, ge=1, le=100),
):
"""Lista todos los usuarios con filtrado y paginación opcionales."""
resultado = list(usuarios_db.values())
if activo is not None:
resultado = [u for u in resultado if u["activo"] == activo]
inicio = (pagina - 1) * limite
return resultado[inicio:inicio + limite]
@app.get("/usuarios/{id}", response_model=UsuarioRespuesta)
def obtener_usuario(id: int):
"""Obtiene un usuario por su ID."""
usuario = usuarios_db.get(id)
if not usuario:
raise HTTPException(status_code=404, detail=f"Usuario {id} no encontrado")
return usuario
@app.post("/usuarios", response_model=UsuarioRespuesta, status_code=201)
def crear_usuario(usuario: UsuarioCrear):
"""
Crea un nuevo usuario.
- **nombre**: nombre completo del usuario
- **email**: dirección de email válida (se usa para notificaciones)
"""
# Verificar email único
if any(u["email"] == usuario.email for u in usuarios_db.values()):
raise HTTPException(status_code=409, detail="El email ya está registrado")
nuevo_id = max(usuarios_db.keys()) + 1
nuevo = {
"id": nuevo_id,
"nombre": usuario.nombre,
"email": usuario.email,
"activo": True,
"creado_en": datetime.now()
}
usuarios_db[nuevo_id] = nuevo
return nuevo
@app.patch("/usuarios/{id}", response_model=UsuarioRespuesta)
def actualizar_usuario(id: int, datos: dict):
if id not in usuarios_db:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
usuarios_db[id].update(datos)
return usuarios_db[id]
@app.delete("/usuarios/{id}", status_code=204)
def eliminar_usuario(id: int):
if id not in usuarios_db:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
del usuarios_db[id]
# Ejecutar:
uvicorn main:app --reload
# FastAPI genera automáticamente:
# Swagger UI: http://localhost:8000/docs
# ReDoc: http://localhost:8000/redoc
# OpenAPI JSON: http://localhost:8000/openapi.json
Dependencias: el sistema de inyección de FastAPI
# El sistema de Depends es uno de los puntos más potentes de FastAPI
# Permite reutilizar lógica (autenticación, BD, validación) de forma limpia
from fastapi import Depends, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
seguridad = HTTPBearer()
def obtener_usuario_actual(
credenciales: HTTPAuthorizationCredentials = Security(seguridad)
) -> dict:
"""Verificar el JWT y devolver el usuario actual."""
try:
payload = jwt.decode(
credenciales.credentials,
SECRET_KEY,
algorithms=["HS256"]
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expirado")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Token inválido")
def requiere_admin(usuario = Depends(obtener_usuario_actual)) -> dict:
if usuario.get("rol") != "admin":
raise HTTPException(status_code=403, detail="Se requieren permisos de admin")
return usuario
# Usar las dependencias en las rutas
@app.get("/admin/estadisticas")
def ver_estadisticas(admin = Depends(requiere_admin)):
return {"total_usuarios": len(usuarios_db)}
@app.get("/perfil")
def ver_perfil(usuario_actual = Depends(obtener_usuario_actual)):
return {"id": usuario_actual["sub"], "email": usuario_actual["email"]}
FastAPI con base de datos: SQLAlchemy asíncrono
# pip install sqlalchemy[asyncio] asyncpg
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
from sqlalchemy import String, Boolean, DateTime
from datetime import datetime
DATABASE_URL = "postgresql+asyncpg://usuario:clave@localhost/mi_db"
engine = create_async_engine(DATABASE_URL)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
class UsuarioModel(Base):
__tablename__ = "usuarios"
id: Mapped[int] = mapped_column(primary_key=True)
nombre: Mapped[str] = mapped_column(String(100))
email: Mapped[str] = mapped_column(String(200), unique=True)
activo: Mapped[bool] = mapped_column(Boolean, default=True)
creado_en: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
async def obtener_sesion():
async with AsyncSessionLocal() as sesion:
yield sesion
# Ruta asíncrona con acceso a base de datos
from sqlalchemy import select
@app.get("/usuarios/{id}", response_model=UsuarioRespuesta)
async def obtener_usuario(id: int, db: AsyncSession = Depends(obtener_sesion)):
resultado = await db.execute(select(UsuarioModel).where(UsuarioModel.id == id))
usuario = resultado.scalar_one_or_none()
if not usuario:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
return usuario
Cuándo elegir FastAPI:
- APIs REST o GraphQL que necesitan documentación automática (OpenAPI/Swagger).
- Servicios que requieren alto rendimiento o manejan muchas conexiones concurrentes.
- Proyectos con Python moderno donde el tipado fuerte reduce errores.
- Microservicios que necesitan ser eficientes y bien documentados.
- Cuando el equipo usa TypeScript en el frontend y valora contratos de API claros.
- Aplicaciones con lógica asíncrona intensiva (llamadas a APIs externas, WebSockets).
Comparativa directa: el mismo endpoint en los tres frameworks
# Crear un usuario con validación, manejo de errores y respuesta tipada
# ─── Flask ────────────────────────────────────────────────────────────────────
@app.route("/usuarios", methods=["POST"])
def crear_usuario():
datos = request.get_json()
# Validación manual
if not datos:
return jsonify({"error": "Se requiere cuerpo JSON"}), 400
if not datos.get("nombre"):
return jsonify({"error": "nombre es obligatorio"}), 400
if not datos.get("email") or "@" not in datos["email"]:
return jsonify({"error": "email inválido"}), 400
# Verificar duplicado
if Usuario.query.filter_by(email=datos["email"]).first():
return jsonify({"error": "Email ya registrado"}), 409
usuario = Usuario(nombre=datos["nombre"], email=datos["email"])
db.session.add(usuario)
db.session.commit()
return jsonify({"id": usuario.id, "nombre": usuario.nombre, "email": usuario.email}), 201
# ─── Django REST Framework ────────────────────────────────────────────────────
class UsuarioViewSet(viewsets.ModelViewSet):
queryset = Usuario.objects.all()
serializer_class = UsuarioSerializer
# create() ya viene implementado en ModelViewSet:
# valida con el serializer, verifica unique constraints,
# guarda en BD y devuelve 201 con el recurso creado.
# No hay que escribir nada más.
# ─── FastAPI ──────────────────────────────────────────────────────────────────
@app.post("/usuarios", response_model=UsuarioRespuesta, status_code=201)
async def crear_usuario(
usuario: UsuarioCrear, # Pydantic valida automáticamente
db: AsyncSession = Depends(obtener_sesion)
):
existente = await db.execute(
select(UsuarioModel).where(UsuarioModel.email == usuario.email)
)
if existente.scalar_one_or_none():
raise HTTPException(status_code=409, detail="Email ya registrado")
nuevo = UsuarioModel(**usuario.model_dump())
db.add(nuevo)
await db.commit()
await db.refresh(nuevo)
return nuevo
# La documentación Swagger se genera sola a partir del tipo de retorno
Rendimiento: ¿cuánto importa realmente?
# Benchmarks aproximados (peticiones por segundo en condiciones similares):
# FastAPI (async): ~40.000 req/s # comparable a Node.js/Go
# Flask (sync): ~8.000 req/s
# Django (sync): ~6.000 req/s
# ⚠️ Estos números son orientativos y dependen enormemente de:
# - La complejidad de la lógica de negocio
# - Las consultas a base de datos (suelen ser el cuello de botella real)
# - El servidor de producción (gunicorn/uvicorn workers, hardware)
# - El tipo de carga (CPU-bound vs IO-bound)
# Para la mayoría de aplicaciones:
# El cuello de botella es la base de datos, no el framework
# Un endpoint que hace 3 consultas SQL de 20ms cada una
# tarda 60ms independientemente del framework (Flask o FastAPI)
# FastAPI marca diferencia real cuando:
# - Manejas miles de conexiones concurrentes simultáneas
# - Tienes IO intensivo (muchas llamadas a APIs externas en paralelo)
# - Necesitas WebSockets a escala
Tabla comparativa
| Característica | Flask | Django | FastAPI |
|---|---|---|---|
| Año de lanzamiento | 2010 | 2005 | 2018 |
| Filosofía | Microframework, elige tú todo | Baterías incluidas, convenciones | APIs modernas con tipado |
| Curva de aprendizaje | Baja | Media-alta | Media |
| Rendimiento | Medio | Medio | Alto |
| Async nativo | Limitado | Limitado | Sí (core) |
| Validación de datos | Manual o con marshmallow | Serializers de DRF | Automática con Pydantic |
| Documentación API | Manual o con flask-swagger | Con DRF browsable API | Automática (Swagger + ReDoc) |
| ORM incluido | No (usar SQLAlchemy) | Sí (Django ORM) | No (usar SQLAlchemy) |
| Panel de administración | No | Sí (incluido) | No |
| Migraciones BD | Flask-Migrate (Alembic) | Incluidas | Alembic |
| Sistema de auth | Flask-Login + extensiones | Incluido y completo | Manual con Depends |
| Ideal para | APIs simples, aprender | Apps web completas, CMS | APIs REST/GraphQL modernas |
Guía de decisión rápida
Elige Django si: necesitas una aplicación web completa con templates, el panel de administración te ahorra semanas de trabajo, quieres que las convenciones del framework tomen decisiones por ti, o tu equipo ya conoce Django.
Elige FastAPI si: construyes una API REST que consumirá un frontend separado (React, Vue, móvil), valoras la documentación automática, usas Python moderno con tipos, necesitas rendimiento asíncrono o estás construyendo microservicios.
Elige Flask si: el proyecto es pequeño y quieres control total sobre cada pieza, estás aprendiendo desarrollo web con Python y quieres entender los fundamentos sin magia, o tienes requisitos muy específicos que otros frameworks no encajan bien.
Resumen
- Flask es el más flexible: viene con lo mínimo y te deja añadir lo que necesitas. Es ideal para proyectos pequeños, para aprender y cuando quieres control total. La contrapartida es que tienes que tomar y mantener más decisiones.
- Django viene con todo: ORM, migraciones, auth, admin, templates. Es la elección más productiva para aplicaciones web completas, especialmente cuando el panel de administración aporta valor inmediato. La contrapartida es que tiene más opiniones y es más difícil salirse de sus convenciones.
- FastAPI está diseñado para APIs modernas: validación automática con Pydantic, documentación Swagger sin configuración, rendimiento asíncrono alto. Es la mejor opción cuando construyes una API que consume un frontend separado y valoras los contratos de tipo entre frontend y backend.
- Para la mayoría de proyectos nuevos que son APIs REST con un frontend separado, FastAPI o Django REST Framework son las opciones más productivas. Para aplicaciones web con frontend en el servidor, Django. Para proyectos pequeños o donde quieres aprender los fundamentos, Flask.
No hay comentarios todavía. Sé el primero en compartir tu opinión.