D/ DJANGO CON CRITERIO

UNIDAD 2 · APIS Y CASOS DE USO

repoGenerico: la biblioteca reutilizable del equipo

Convierte los flujos repetidos de DRF en una base común para tus aplicaciones y proyectos.

Ilustración: Un molde con puntos de extensión
Un molde con puntos de extensión

CAPÍTULO 3 · Cada pieza encuentra su responsabilidad

De reconocer la repetición a construir repoGenerico

Al mirar varias queries y commands juntas, el patrón se vuelve evidente. Cambian el modelo, el serializer o los campos de búsqueda, pero una parte importante del trabajo permanece igual. Consultar por ID requiere encontrar un objeto y representarlo; crear requiere validar y guardar. Si copiamos ese flujo, también tendremos que copiar cada corrección futura.

De esa necesidad nace repoGenerico, una capa propia del equipo sobre Django REST Framework. El algoritmo común vive en clases base y la aplicación declara sus diferencias. Por eso una consulta de ubicaciones puede quedar reducida a modelo, serializer y permisos. La lógica no se perdió: quedó reunida donde puede mantenerse y probarse una vez para varios consumidores.

DRF ya dispone de herramientas genéricas; nuestra biblioteca concentra además las convenciones del equipo. Trasladarla a otro proyecto permite conservar una forma de trabajar, siempre revisando sus contratos y dependencias. Uno de esos contratos exige especial cuidado: si cada nueva tabla inventa cómo paginar, pronto volveremos a tener versiones diferentes del mismo comportamiento.

Leer esta parte dentro del relato completo →

DEL RELATO A LA PRÁCTICA

Vamos a desarrollar esta decisión con su mapa, explicación, código y ejercicio.

MIRA · CONECTA · COMPRENDE

Un molde con puntos de extensión

Un molde con puntos de extensión1. Vista genérica: Define el flujo común; 2. Queryset: Selecciona los datos; 3. Serializer: Define la representación; 4. Respuesta: Conserva el contrato compartido01Vista genéricaDefine el flujo común02QuerysetSelecciona los datos03SerializerDefine la representación04RespuestaConserva el contrato compartido

Las flechas muestran responsabilidades relacionadas; no representan un orden obligatorio de ejecución.

Selecciona una pieza para destacar su responsabilidad.

Leer la explicación del mapa
  1. Vista genérica. Define el flujo común
  2. Queryset. Selecciona los datos
  3. Serializer. Define la representación
  4. Respuesta. Conserva el contrato compartido
Descargar gráfico SVG ↓

¿Para qué sirve?

Al crear varios módulos, el equipo encontraba una y otra vez los mismos pasos: buscar un registro, validar datos, guardar, paginar y construir respuestas. Copiarlos aumentaba las líneas y multiplicaba los lugares que había que corregir. La necesidad fue conservar un flujo común y declarar únicamente lo que cambia.

El concepto, explicado

repoGenerico es una carpeta de código propio del equipo, construida sobre Django REST Framework. No es una librería que venga con Django ni un producto de DRF. Su nombre identifica el conjunto de funcionalidades que el autor traslada a nuevos proyectos. Aquí enseñamos esa práctica con SensorHub.

DRF ya ofrece GenericAPIView, mixins, vistas CRUD y clases de paginación. La capa del equipo añade acuerdos propios: nombres de clases, respuestas JSON, parámetros, límites, integración con tablas y puntos de extensión. Se puede construir sobre esas herramientas; no hace falta asumir que DRF obliga a escribir todo desde cero.

La primera señal es visual: varios handlers repiten el mismo algoritmo y solo cambian el modelo, serializer, permisos o filtros. Se extrae ese algoritmo una vez. Una función sirve para una transformación pequeña; una clase base encaja cuando varios pasos cooperan con request, permisos y serializers.

Una Query concreta declara el qué: modelo, contrato de datos, alcance autorizado y búsqueda. La base ejecuta el cómo compartido: validar parámetros, consultar, ordenar, paginar, serializar y responder. Template Method describe ese flujo con puntos de extensión; DRY evita duplicación de conocimiento y responsabilidad única ayuda a ubicar cada cambio.

El nombre repositorio genérico es la convención del equipo. Estas clases son adaptadores HTTP sobre el ORM; no equivalen por sí solas al patrón Repository que desacopla persistencia mediante una interfaz. Los servicios conservan las reglas del negocio y UTIL_LIB los cálculos puros.

La reducción de código ocurre en cada consumidor. La lógica sigue existiendo en la base y allí necesita pruebas. El beneficio crece cuando una corrección compartida llega a todas las vistas que realmente heredan el flujo, sin pegarla en cada módulo.

Herencia no garantiza reutilización: si una subclase redefine post completo, el post de la base ya no se ejecuta salvo que lo invoque. Los mensajes, auditoría o prioridad de errores repetidos pueden convertirse en hooks explícitos, siempre que la base los llame. No inventes un perform_create o perform_update que la clase padre nunca ejecuta.

Esta academia ejecuta un subconjunto público: BaseListQueryView y CatalogQuery. El inventario siguiente describe la familia de clases examinada en la base del equipo; no afirma que todos esos endpoints CRUD estén publicados aquí. Los ejemplos de ubicaciones son didácticos y requieren modelo, serializer y política en la aplicación que los integre.

Mapa de operaciones de la biblioteca del equipo

PiezaOperación y contratoQué aporta la aplicación
BaseReadForIdViewGET por obj_id; success + data o errorModelo, serializer y permisos; helper de búsqueda por ID
BaseRetrieveViewGET por varios nombres de ID en URL; success + dataget_queryset para alcance y optimización
BaseListViewLista paginada: DataTables, JSON convencional o template HTMLModelo, serializer, permisos, búsqueda, orden y columnas
BaseListQueryViewLista paginada JSON sin DataTables; results y metadatosQueryset, búsqueda y orden del módulo
BaseListAllViewTodos los registros del conjunto; success + results + countConjunto explícitamente acotado y orden permitido
BaseCreateViewPOST: validar y guardar; success + message + id; errores 400Serializer y permisos; extensiones si la base las invoca
BaseRetrieveUpdateViewGET / PUT por ID; PUT usa partial=True en esta implementaciónModelo, serializer y control de acceso
BaseDeleteViewDELETE por ID; success + messageModelo, permisos y get_object_info
ValidationErrorFormatterMensaje desde errores del serializer, con priority_fieldsPrioridad de campos; no sustituye la validación
parse_pagination_params / MAX_PAGE_SIZEConversión y límites comunes; máximo 100Parámetros de entrada y tamaño por defecto
invalid_pagination_response / internal_error_responseErrores uniformes sin exponer detalles internosRegistro interno y contrato de respuesta
Helpers de ID y objetosget_object_by_id, get_or_create_object y _get_object_id_from_kwargsReutilizan búsqueda/resolución de ID; get_or_create puede escribir

VER LA ABSTRACCIÓN

Antes y después de compartir el flujo

Consultar por ID: dónde desaparece la repetición

Comparación didáctica del mismo flujo mínimo, con imports omitidos en ambos lados. Supone un queryset ya autorizado; el ejemplo no agrega por sí solo autorización por objeto. No se compara contra las vistas genéricas nativas de DRF, que también reducen código.

Antes · repetido en el consumidor

class UbicacionPorIdQuery(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, obj_id):
        try:
            obj = Ubicacion.objects.get(id=obj_id)
        except Ubicacion.DoesNotExist:
            return Response(
                {"success": False, "error": "Objeto no encontrado"},
                status=400,
            )
        serializer = UbicacionSerializer(obj)
        return Response({"success": True, "data": serializer.data})

Después · base y diferencias explícitas

class UbicacionPorIdQuery(BaseReadForIdView):
    model = Ubicacion
    serializer_class = UbicacionSerializer
    permission_classes = [IsAuthenticated]

La búsqueda, serialización y respuesta pasan a la base. Se conserva aquí el 400 del contrato por ID examinado para que la comparación sea honesta; si se decide migrar a 404, hay que versionar y probar el contrato compartido. El código común no desapareció: dejó de repetirse.

Crear: cómo evitar repetir también los mensajes

Propuesta de extensión de la biblioteca, no una afirmación de que estos hooks ya existan en la base examinada. El serializer y las clases mencionadas deben importarse en el módulo correspondiente.

Antes · repetido en el consumidor

# Cada Command vuelve a escribir post:
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
    obj = serializer.save()
    return Response({"success": True,
                     "message": "Ubicación creada", "id": obj.id}, status=201)
return Response({"success": False,
    "error": ValidationErrorFormatter.format_error(
        serializer.errors, priority_fields=["nombre"])
}, status=400)

Después · base y diferencias explícitas

# UNA vez en la base mejorada:
class BaseCreateView(APIView):
    success_message = "Registro creado"
    priority_fields = ()

    def post(self, request):
        serializer = self.serializer_class(data=request.data)
        if not serializer.is_valid():
            return Response({"success": False,
                "error": ValidationErrorFormatter.format_error(
                    serializer.errors, self.priority_fields)
            }, status=400)
        obj = serializer.save()
        return Response({"success": True,
            "message": self.success_message, "id": obj.id}, status=201)

# En CADA aplicación:
class CrearUbicacionCommand(BaseCreateView):
    serializer_class = UbicacionSerializer
    permission_classes = [IsAuthenticated, CanManageLocations]
    success_message = "Ubicación creada"
    priority_fields = ("nombre",)

El bloque compartido se escribe una vez; el consumidor solo configura diferencias. CanManageLocations representa la política que debe implementar el proyecto. Para auditoría o asignación de tenant, diseña un hook invocado por la base y prueba su comportamiento.

DECISIONES DE LA BIBLIOTECA COMPARTIDA

Puntos de extensión: cambiar una parte sin reescribir todo

get_queryset delimita los registros y permite select_related/prefetch_related. Aplica allí el alcance de usuario o tenant antes de contar, buscar o recuperar objetos.

apply_search_filters expresa qué significa buscar en ese módulo. get_default_ordering y get_allowed_ordering_fields definen orden y campos permitidos; datatables_columns traduce columnas visibles a campos del servidor, con None para acciones no ordenables.

paginate_queryset y los métodos de respuesta pertenecen al contrato común. Sobrescribirlos requiere justificar una variación y probar clientes existentes. get_context_data permite aportar contexto al modo HTML.

get_object_info permite describir el objeto eliminado. ValidationErrorFormatter prioriza mensajes, pero no comprueba identidad ni reglas de negocio.

BaseReadForIdView también contiene get_or_create_object. Aunque esté ubicado junto a una clase de lectura, puede insertar datos: usarlo pertenece a una intención de comando, no a una consulta sin efectos. Los helpers de ID aceptan distintos nombres de URL; documenta cuál usa cada endpoint.

Qué revisar antes de llevar la biblioteca a otro proyecto

Mantén versiones o etiquetas y una lista de cambios. Copiar la carpeta permite empezar rápido, pero las copias no reciben actualizaciones automáticamente. Un paquete interno versionado o una plantilla puede facilitar el mantenimiento cuando haya varios proyectos.

Confirma compatibilidad de Django/DRF, parámetros de URL, identificadores, formato del serializer y política de permisos. BaseListAllView requiere un conjunto pequeño y acotado; un histórico creciente necesita paginación o exportación por lotes.

Revisa las excepciones: una captura general puede convertir un 404 en 500 o esconder un error de programación. Define qué devuelve cada situación y conserva detalles solo en logs internos.

Revisa actualización parcial: la base examinada acepta PUT con partial=True; no lo confundas con reemplazo completo ni anuncies soporte PATCH inexistente. Si cambias esa semántica, migra el contrato.

Las bases no garantizan acceso por objeto. Una aplicación que sirve varios clientes necesita queryset autorizado y comprobación de permisos de objeto en detalle, actualización y eliminación. La reutilización no sustituye esas garantías.

Implementación paso a paso

  1. Comparar dos vistas del mismo tipo y marcar los bloques realmente repetidos.
  2. Elegir una base existente antes de crear un handler nuevo.
  3. Definir contrato de respuesta, errores, permisos, parámetros y límites.
  4. Mover el flujo estable a la base y declarar hooks que sí se ejecuten.
  5. Reducir las subclases a modelo, serializer, política y diferencias del negocio.
  6. Probar dos consumidores distintos y una corrección compartida.
  7. Versionar la biblioteca y revisar compatibilidad antes de trasladarla a otro proyecto.

Ejemplo en SensorHub

# _AppComplementos/views/queries/ubicaciones.py
# Ejemplo didáctico con la interfaz de la biblioteca del equipo.
from rest_framework.permissions import IsAuthenticated
from repoGenerico.views_base import BaseListView, BaseReadForIdView
from _AppComplementos.models import Ubicacion
from _AppComplementos.serializers import UbicacionSerializer

class UbicacionPorIdQuery(BaseReadForIdView):
    model = Ubicacion
    serializer_class = UbicacionSerializer
    permission_classes = [IsAuthenticated]

class UbicacionesPaginadasQuery(BaseListView):
    model = Ubicacion
    serializer_class = UbicacionSerializer
    permission_classes = [IsAuthenticated]
    default_ordering = "nombre"
    datatables_columns = ["nombre", "latitud", "longitud", None]

    def get_allowed_ordering_fields(self):
        return ["nombre", "latitud", "longitud"]

    def apply_search_filters(self, queryset, search_query):
        return queryset.filter(nombre__icontains=search_query)

# La subclase NO redefine get ni crea otro Paginator.
# IsAuthenticated identifica una condición mínima; añade la capacidad y
# alcance por objeto que requiera tu negocio antes de publicar la API.

Cómo leer el ejemplo

  • model y serializer_class son los datos variables del algoritmo.
  • Los permisos se declaran en la aplicación y se verifican en el servidor.
  • La consulta paginada modifica búsqueda y campos, pero hereda la mecánica HTTP.
  • La clase por ID no escribe get: el comportamiento está en la base.
  • Si un cambio aplica a todos los consumidores, se propone y prueba en repoGenerico.

Error habitual

Una base gigantesca acopla módulos; una subclase que copia get/post completo pierde el beneficio. Abstrae lo repetido y comprueba qué método se ejecuta realmente.

05 / PRACTICAR

Tu turno

Ubicaciones y sistemas repiten post solo para cambiar success_message y priority_fields. Diseña dos puntos de extensión sin duplicar validación ni guardado.

Ver solución orientativa

La base declara success_message y priority_fields y su post los usa al construir la respuesta y formatear errores. Las subclases solo asignan esos atributos. Prueba creación válida e inválida en ambos consumidores. Si hay reglas de negocio distintas, delega a un servicio en lugar de esconderlas en la base.

Cómo sabes que funciona

  • La Query o Command indica qué varía sin copiar el algoritmo compartido.
  • Cada hook declarado se invoca realmente desde la base.
  • Modelo, serializer, permisos y queryset siguen siendo explícitos.
  • Los consumidores comparten respuestas y límites comprobables.

Para profundizar

Documentación oficial para comprobar configuración, permisos y condiciones vigentes. Revisión del tema: 2026-10-12.

CONTINUAR LA EXPERIENCIA

El siguiente paso del recorrido:

Paginación desde repoGenerico: un solo contrato →Continuar en modo relato