Saltar al contenido

# creacion-de-aplicaciones-web-en-django-y-python

Creación de Aplicaciones Web en Django y Python

Volver a la portada del curso
Lectura (Markdown)

Construcción de un Backend

Sección 5 · Proyecto Final

Construcción de un backend

El backend es la parte de la aplicación que el usuario no ve: guarda los datos, decide quién puede hacer qué y responde a cada petición. En la tienda de PyShop ya existen todas las piezas por separado. Esta lectura las ordena en capas, porque el proyecto final consiste justamente en agregar funciones nuevas a cada una sin romper las demás.

Las capas del backend

  1. Datos: los modelos (Categoria, Producto, Resena) y sus migraciones. Definen qué se guarda y qué reglas cumple cada campo.
  2. Lógica: las vistas. Leen la petición, consultan con el ORM, validan con formularios y deciden la respuesta.
  3. Rutas: urls.py conecta cada URL con su vista.
  4. Presentación: plantillas HTML para el navegador y JSON para el código que corre en el navegador o en otra aplicación.
  5. Seguridad: autenticación, permisos y protección CSRF, en gran parte a cargo de los middleware.
  6. Pruebas: el código que comprueba que todo lo anterior sigue funcionando.

Una regla práctica: las reglas sobre los datos (rangos válidos, campos obligatorios) van en el modelo; la vista solo coordina. Así el formulario, el admin y cualquier vista futura las respetan sin repetir código. La calificación de 1 a 5 de Resena funciona así: el validador vive en el modelo y ResenaForm lo hereda.

Endpoints JSON

Una vista no tiene que devolver HTML. api_productos devuelve datos para que el frontend los pinte con JavaScript:

def api_productos(request):
    productos = Producto.objects.filter(disponible=True).select_related("categoria")
    datos = [
        {
            "nombre": p.nombre,
            "categoria": p.categoria.nombre,
            "precio": float(p.precio),
            "url": p.get_absolute_url(),
        }
        for p in productos
    ]
    return JsonResponse({"productos": datos})

La respuesta tiene tipo application/json y 7 productos. Devolver un diccionario con una clave ("productos") en lugar de una lista suelta deja lugar para agregar datos después, como el total o la página, sin romper a quien ya consume la API. Para APIs grandes existe Django REST framework, un paquete aparte; para un catálogo como este, JsonResponse basta.

Autenticación y permisos

django.contrib.auth ya trae usuarios, contraseñas con hash y las vistas de login y logout (include("django.contrib.auth.urls")). En el backend se usan tres niveles:

  • ¿Hay sesión? @login_required manda a los anónimos al login (302) y conserva el destino en next.
  • ¿Quién es? request.user identifica al usuario: la reseña toma su autor de ahí, nunca de un campo del formulario que cualquiera podría alterar.
  • ¿Qué puede hacer? is_staff abre el panel de administración y los permisos por modelo limitan qué edita cada persona. Un anónimo que entra a /admin/ recibe 302 a /admin/login/?next=/admin/.

Consultas eficientes

Si una plantilla muestra la categoría de cada producto, cada acceso a p.categoria puede lanzar una consulta nueva. Medido con CaptureQueriesContext sobre los 7 productos disponibles:

from django.db import connection
from django.test.utils import CaptureQueriesContext

with CaptureQueriesContext(connection) as ctx:
    filas = [(p.nombre, p.categoria.nombre)
             for p in Producto.objects.filter(disponible=True)]
print(len(ctx.captured_queries))   # 8: 1 para productos + 7 para categorías

with CaptureQueriesContext(connection) as ctx:
    filas = [(p.nombre, p.categoria.nombre)
             for p in Producto.objects.filter(disponible=True).select_related("categoria")]
print(len(ctx.captured_queries))   # 1: un JOIN trae todo

Es el problema de las «N+1 consultas»: con 7 productos no se nota, con 7000 sí. select_related sirve para ForeignKey; para relaciones inversas o de muchos a muchos, como las reseñas de cada producto, existe prefetch_related.

Pruebas automáticas

TestCase crea una base de datos de prueba vacía, ejecuta cada prueba dentro de una transacción y la deshace al terminar. self.client es el mismo cliente de pruebas de las lecciones anteriores:

from django.test import TestCase


class CatalogoTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        ropa = Categoria.objects.create(nombre="Ropa", slug="ropa")
        Producto.objects.create(categoria=ropa, nombre="Gorra PyShop",
                                slug="gorra-pyshop", precio="249.00")

    def test_lista(self):
        r = self.client.get("/")
        self.assertContains(r, "Gorra PyShop")

    def test_resena_sin_sesion_redirige(self):
        r = self.client.get("/producto/gorra-pyshop/resena/")
        self.assertRedirects(r, "/cuentas/login/?next=/producto/gorra-pyshop/resena/")

Se ejecutan con python manage.py test catalogo. Una prueba del 404 de un slug inexistente completa el trío básico: lo que debe funcionar, lo que debe estar protegido y lo que no existe.

Trampas comunes

  • Lógica de negocio en las plantillas. Calcular promedios o filtrar en el HTML la vuelve difícil de probar; se calcula en la vista o en el modelo.
  • Confiar en datos del cliente para el autor, el precio o los permisos. Salen del servidor (request.user, la base).
  • Bucles con consultas ocultas. Revisa el número de consultas de las vistas con listas.
  • Probar solo el camino feliz. Los 302 sin sesión, los 404 y los formularios inválidos son donde aparecen los errores.

El notebook tienda-pyshop-completa recorre estas capas archivo por archivo, mide las consultas y ejecuta las pruebas. Úsalo como punto de partida del proyecto final.

Recursos