FastAPI vs Flask vs Django: cuál elegir para tu proyecto

D
DanisCh
(Actualizado: ) • 14 min de lectura
FastAPI vs Flask vs Django: cuál elegir para tu proyecto
Python

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ísticaFlaskDjangoFastAPI
Año de lanzamiento201020052018
FilosofíaMicroframework, elige tú todoBaterías incluidas, convencionesAPIs modernas con tipado
Curva de aprendizajeBajaMedia-altaMedia
RendimientoMedioMedioAlto
Async nativoLimitadoLimitadoSí (core)
Validación de datosManual o con marshmallowSerializers de DRFAutomática con Pydantic
Documentación APIManual o con flask-swaggerCon DRF browsable APIAutomática (Swagger + ReDoc)
ORM incluidoNo (usar SQLAlchemy)Sí (Django ORM)No (usar SQLAlchemy)
Panel de administraciónNoSí (incluido)No
Migraciones BDFlask-Migrate (Alembic)IncluidasAlembic
Sistema de authFlask-Login + extensionesIncluido y completoManual con Depends
Ideal paraAPIs simples, aprenderApps web completas, CMSAPIs 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.

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a