¿Qué es Django?
Django es un framework web para Python con “baterías incluidas”. Mientras que la mayoría de los frameworks te piden elegir una capa de base de datos, un motor de plantillas, un panel de administración y un sistema de autenticación, Django te entrega todo eso en un paquete coherente. Tú describes tus datos y tus páginas, y el framework se encarga de las partes tediosas y sensibles a la seguridad.
Fue creado en 2003 para la redacción de un periódico: un equipo que necesitaba lanzar funcionalidades rápidamente sin colapsar bajo la presión de las fechas de entrega. Ese origen es evidente. Django favorece la convención, la explicitud y una actitud de “debería haber una única forma obvia de hacer las cosas”, y se ha mantenido como una de las opciones más fiables para sitios con mucho contenido y basados en datos.
Si alguna vez has pasado una semana configurando un ORM, una herramienta de migraciones y un sistema de autenticación, Django es el framework que te ahorra esa semana.
El patrón MTV
Django denomina a su arquitectura MTV: Model, Template, View. Es la misma idea que el MVC pero con nombres diferentes, y esa diferencia suele confundir a la gente al principio.
- Un model es una clase de Python que describe una tabla y sus filas.
- Un template es un archivo HTML con marcadores de posición que renderiza los datos.
- Una view es una función o clase que recibe una solicitud, pide datos a los modelos y devuelve una respuesta.
El controlador, en el sentido clásico de MVC, es el propio Django. El despachador de URLs decide qué vista se ejecuta y el framework actúa como mediador entre las tres capas. Una vez que este diagrama queda claro, todo lo demás en Django pasa a ser un detalle en lugar de un misterio.
Modelos y el ORM
Un modelo es una clase que hereda de models.Model. Cada atributo es un campo, y cada campo se convierte en una columna.
# blog/models.py
from django.conf import settings
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
body = models.TextField()
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="posts",
)
published_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return self.title
CharField necesita un max_length porque se mapea a un VARCHAR acotado. TextField no tiene límite. auto_now_add=True establece un valor solo al crear el registro, mientras que auto_now=True se actualiza en cada guardado. null controla la base de datos y blank controla la validación del formulario; están separados a propósito.
Las relaciones son explícitas:
ForeignKeyes de muchos a uno.ManyToManyFieldcrea una tabla intermedia (join table).OneToOneFieldes una clave foránea única.
El related_name define el accesor inverso, para que user.posts.all() funcione desde el otro lado. El argumento on_delete es obligatorio porque Django se niega a adivinar qué debería pasar con los hijos cuando se elimina un padre.
Migraciones
Las migraciones son archivos controlados por versiones que describen los cambios en tu esquema. Tú escribes el modelo y Django escribe el SQL.
python manage.py makemigrations blog
python manage.py migrate
makemigrations inspecciona tus modelos y genera una migración; migrate aplica las migraciones pendientes a la base de datos. Debido a que los archivos de migración residen en tu repositorio, cada entorno termina con el mismo esquema y los cambios pueden revisarse como cualquier otro código. Nunca edites una migración ya aplicada; en su lugar, añade una nueva.
Querysets y relaciones
El ORM devuelve querysets, los cuales son perezosos (lazy): no se ejecutan hasta que los iteras, los recortas (slice) o los evalúas. Esta característica te permite encadenar filtros y ejecutar únicamente la consulta final.
Post.objects.filter(status="PB").exclude(author=user).order_by("-published_at")[:10]
Entre los métodos más útiles de los querysets se encuentran filter, exclude, get, first, exists, count, values, values_list, annotate y aggregate. Las expresiones F() te permiten hacer referencia a una columna de la base de datos, y los objetos Q() te permiten construir condiciones OR.
from django.db.models import Count, Q
published = Post.objects.filter(
Q(status="PB") | Q(published_at__isnull=False)
).annotate(comment_count=Count("comments"))
Aquí es donde el ORM demuestra su valor: las consultas complejas permanecen en Python, se parametrizan de forma segura y son componibles.
Cómo evitar el problema N+1
El error de rendimiento más común en Django es la consulta N+1. Esto ocurre cuando accedes a un objeto relacionado dentro de un bucle, lo que dispara una consulta adicional por cada fila.
# 1 query for posts, then 1 query per post for the author
for post in Post.objects.all():
print(post.author.username)
select_related integra las relaciones de valor único en la consulta original, mientras que prefetch_related ejecuta una segunda consulta y une los resultados en Python para las relaciones de valores múltiples.
# 1 query for posts joined with author, 1 for the comments
posts = Post.objects.select_related("author").prefetch_related("comments")
Utiliza select_related para foreign keys y campos one-to-one, y prefetch_related para relaciones many-to-many y relaciones inversas. Si notas que una página es lenta, cuenta las consultas con el Django Debug Toolbar antes de optimizar cualquier otra cosa.
Vistas: funciones y clases
Una vista es cualquier elemento ejecutable (callable) que recibe una solicitud y devuelve una respuesta. Las vistas basadas en funciones son el punto de partida más claro.
# blog/views.py
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, render
from .models import Post
def post_list(request):
posts = Post.objects.filter(status="PB").select_related("author")
return render(request, "blog/post_list.html", {"posts": posts})
def post_api(request, slug):
post = get_object_or_404(Post, slug=slug)
return JsonResponse({"title": post.title, "body": post.body})
Las vistas basadas en clases sacrifican un poco de simplicidad en favor de la reutilización. ListView, DetailView, CreateView, UpdateView y DeleteView implementan los patrones CRUD estándar, y los mixins permiten componer el comportamiento.
from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
model = Post
template_name = "blog/post_list.html"
context_object_name = "posts"
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(status="PB").select_related("author")
Utiliza vistas basadas en funciones para lógica puntual y vistas basadas en clases genéricas para páginas estándar de listado y detalle. Mezclar ambas en una base de código es normal y saludable.
URLs
El despachador de URLs mapea una ruta a una vista. Los patrones residen en urls.py, y cada aplicación puede mantener los suyos propios.
# config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls")),
]
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.post_list, name="post_list"),
path("<slug:slug>/", views.post_detail, name="post_detail"),
]
Los conversores como <int:id> y <slug:slug> validan y tipan el segmento capturado. Nombra siempre tus rutas y revierte el proceso con reverse() o la etiqueta {% url %} en lugar de escribir las rutas a mano, para que los enlaces sobrevivan a un cambio de nombre.
Templates
Los templates son HTML con un lenguaje pequeño e intencionalmente limitado. El autoescaping está activado por defecto, que es lo que te protege contra XSS.
{% extends "base.html" %}
{% block content %}
<h1>{{ post.title }}</h1>
<p>By {{ post.author.username }} on {{ post.published_at|date:"F j, Y" }}</p>
<div>{{ post.body|linebreaks }}</div>
{% endblock %}
{% extends %} permite la herencia de templates, {% include %} comparte fragmentos, y los filtros como date, linebreaks y default formatean los valores. No se permite Python arbitrario en los templates; si un template necesita lógica, dicha lógica debe residir en la vista o en el modelo.
Formularios y validación
El sistema de formularios de Django valida la entrada, renderiza el HTML y protege contra manipulaciones mediante un token CSRF.
# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "slug", "body", "status", "published_at"]
widgets = {"body": forms.Textarea(attrs={"rows": 12})}
# blog/views.py
from django.shortcuts import redirect, render
from .forms import PostForm
def post_create(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect(post)
else:
form = PostForm()
return render(request, "blog/post_form.html", {"form": form})
Un ModelForm deriva los campos y la validación del modelo, evitando que ambos se desincronicen. Los métodos de limpieza como clean_slug añaden reglas personalizadas, y form.is_valid() recopila todos los errores antes de interactuar con la base de datos.
El admin
El admin es la funcionalidad más infravalorada de Django. Registra un modelo y obtendrás una interfaz para el personal que permite realizar búsquedas, aplicar filtros y gestionar permisos.
# blog/admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "author", "status", "published_at"]
list_filter = ["status", "created_at"]
search_fields = ["title", "body"]
prepopulated_fields = {"slug": ["title"]}
raw_id_fields = ["author"]
date_hierarchy = "published_at"
Eso es una herramienta interna completa en apenas una docena de líneas. Para sitios de contenido, el admin es a menudo el producto en sí mismo. También actúa como un límite de seguridad: los permisos del personal, has_add_permission y has_change_permission controlan exactamente qué puede hacer cada rol.
Configuración y apps
Un proyecto de Django es una colección de apps, donde cada una es un paquete autónomo con sus propios modelos, vistas, plantillas y migraciones. INSTALLED_APPS las enumera, y settings.py contiene la configuración.
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"blog",
]
Mantén la configuración fuera del control de versiones para proteger los secretos. Léelos desde variables de entorno, divide la configuración por entorno si el proyecto crece y utiliza django-environ o pydantic-settings para procesarlos. Los comandos django-admin startproject y startapp generan el andamiaje de ambas capas, por lo que la estructura nunca está en duda.
Valores predeterminados de seguridad
Los valores predeterminados de Django están diseñados para que la opción más segura sea también la más sencilla:
- Los tokens CSRF protegen los formularios que modifican el estado; el middleware rechaza las solicitudes que no los incluyan.
- Se evita la SQL injection ya que los querysets parametrizan los valores.
- Se mitiga el XSS porque las plantillas escapan las variables, a menos que decidas desactivarlo con
|safe. - Las contraseñas se procesan mediante un hash con un algoritmo moderno y hashers configurables.
- El Clickjacking se bloquea mediante el middleware
X-Frame-Options. - El uso de HTTPS se impone en producción mediante
SECURE_SSL_REDIRECT,SESSION_COOKIE_SECUREyCSRF_COOKIE_SECURE.
Ejecuta python manage.py check --deploy antes de desplegar a producción. Te avisará sobre los ajustes que hayas olvidado y es mucho más económico que realizar una auditoría de seguridad.
Creación de APIs con Django REST Framework
Para APIs JSON, Django REST Framework (DRF) es el complemento estándar. Un serializer describe cómo un modelo se convierte en JSON y viceversa, mientras que un viewset junto con un router generan los endpoints CRUD.
# blog/serializers.py
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField()
class Meta:
model = Post
fields = ["id", "title", "slug", "author", "status", "published_at"]
# blog/api.py
from rest_framework import viewsets
from .models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.select_related("author")
serializer_class = PostSerializer
lookup_field = "slug"
# blog/urls.py
from rest_framework.routers import DefaultRouter
from .api import PostViewSet
router = DefaultRouter()
router.register("posts", PostViewSet, basename="post")
urlpatterns = router.urls
Los serializers validan la entrada de la misma forma que lo hacen los formularios, los viewsets proporcionan las acciones de list, create, retrieve, update y delete de forma automática, y DRF puede generar esquemas OpenAPI para tu API. El ORM de Django y las convenciones de DRF representan un camino consolidado para APIs en producción.
Pruebas
El test runner de Django crea una base de datos de prueba limpia y proporciona un cliente que interactúa con todo el pipeline de peticiones.
# blog/tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from .models import Post
class PostDetailTests(TestCase):
def setUp(self):
user = get_user_model().objects.create_user("ada", password="secret")
self.post = Post.objects.create(
title="Hello",
slug="hello",
body="First post",
author=user,
status=Post.Status.PUBLISHED,
)
def test_detail_page_renders(self):
response = self.client.get(reverse("blog:post_detail", args=["hello"]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Hello")
def test_missing_post_returns_404(self):
response = self.client.get(reverse("blog:post_detail", args=["nope"]))
self.assertEqual(response.status_code, 404)
TestCase envuelve cada prueba en una transacción y hace un rollback, para que las pruebas permanezcan aisladas y sean rápidas. Usa django.test.Client para las vistas, el ORM directamente para los modelos, y pytest-django si prefieres los fixtures de pytest. Intenta probar el comportamiento y los permisos, no los detalles de implementación.
Mejores prácticas
- Mantén los modelos enfocados en los datos y en métodos de dominio pequeños; deja la orquestación para los servicios o las vistas.
- Ejecuta siempre
select_relatedyprefetch_relatedcuando una plantilla acceda a relaciones. - Utiliza
ModelFormy serializers de DRF para que la validación resida en un solo lugar. - Asigna un nombre a cada URL y utiliza el reverse; nunca escribas rutas fijas (hardcode) en las plantillas o en el código.
- Añade índices de base de datos en las columnas que utilices para filtrar y ordenar.
- Guarda los secretos en variables de entorno y ejecuta
check --deployantes del lanzamiento. - Escribe las migraciones como artefactos de code review y nunca edites una que ya haya sido aplicada.
- Prueba el pipeline de peticiones con
Clienty la lógica de dominio de forma directa.
Errores comunes
- Iterar sobre un queryset y acceder a relaciones dentro del bucle, provocando consultas N+1.
- Usar
null=Trueen un campo de texto en lugar de una cadena vacía, y luego luchar contra las validaciones deNone. - Colocar lógica de negocio en las plantillas porque la vista parecía demasiado saturada.
- Llamar a
save()dentro de un bucle en lugar de usarbulk_createobulk_update. - Olvidar
on_deleteo elegirCASCADEsin considerar la posible pérdida de datos. - Dejar
DEBUG = Truey unSECRET_KEYhardcodeado en producción. - Tratar el admin como un frontend orientado al usuario final en lugar de una herramienta para el staff.
- Realizar consultas con
len(queryset)en lugar de.count(), cargando cada fila en memoria.
Próximos pasos
Django te ofrece el camino completo y convencional para crear una aplicación web de Python lista para producción. Si prefieres un núcleo más ligero y mayor flexibilidad, lee a continuación la guía de Flask. Si estás construyendo una API JSON asíncrona moderna y quieres que los tipos se encarguen de la validación, pasa a FastAPI. Elijas lo que elijas, invierte en la base de datos: el indexado, las transacciones y la planificación de consultas importan más que el framework, y el backend roadmap profundiza en ello. La guía de REST APIs cubre las reglas de diseño que hacen que una API sea agradable de consumir.