[{"content":"En los equipos se escucha con frecuencia \u0026ldquo;tenemos una API REST\u0026rdquo;. Pero al revisar las respuestas JSON, no hay ningún enlace. Solo datos en crudo. Eso no es REST, es CRUD expuesto en HTTP.\nLa diferencia la marca un principio que la mayoría de los desarrolladores ignora: HATEOAS.\n¿Qué es HATEOAS en una API REST? HATEOAS significa Hypermedia As The Engine Of Application State. Es una de las restricciones fundamentales del REST, definida por Roy Fielding en su tesis del año 2000.\nEl principio es claro: un cliente REST no debería necesitar conocer de antemano las rutas de la API. Descubre las acciones disponibles siguiendo los enlaces que proporciona cada respuesta. Igual que navegamos en la web.\nCRUD sobre HTTP frente al REST real Sin HATEOAS, el cliente tiene que conocer de antemano las rutas. Ese conocimiento está codificado en duro. Si la API cambia una ruta, el cliente se rompe. No es evolución, es acoplamiento fuerte disfrazado.\nUna respuesta HATEOAS real Con HATEOAS, la respuesta se vuelve autodescriptiva gracias a _links. El cliente lee los enlaces disponibles y sabe qué acciones son posibles en el estado actual. Si el contrato ya está validado, el enlace valider no aparece.\nHATEOAS en la práctica: los enlaces reflejan el estado del recurso Los enlaces cambian según el estado del recurso. El cliente no necesita codificar lógica condicional. La API conduce el estado de la aplicación.\nImplementación con Django REST Framework y HATEOAS DRF no incluye HATEOAS de forma nativa. Se implementa con un serializer:\nfrom rest_framework import serializers from .models import Contrat, ContratStatus class ContratSerializer(serializers.ModelSerializer): _links = serializers.SerializerMethodField() class Meta: model = Contrat fields = [\u0026#39;id\u0026#39;, \u0026#39;status\u0026#39;, \u0026#39;montant\u0026#39;, \u0026#39;client_id\u0026#39;, \u0026#39;_links\u0026#39;] def get__links(self, obj: Contrat) -\u0026gt; dict[str, dict[str, str]]: base = f\u0026#39;/api/contrats/{obj.pk}/\u0026#39; links: dict[str, dict[str, str]] = { \u0026#39;self\u0026#39;: {\u0026#39;href\u0026#39;: base, \u0026#39;method\u0026#39;: \u0026#39;GET\u0026#39;}, } if obj.status == ContratStatus.PENDING: links[\u0026#39;valider\u0026#39;] = {\u0026#39;href\u0026#39;: f\u0026#39;{base}valider/\u0026#39;, \u0026#39;method\u0026#39;: \u0026#39;POST\u0026#39;} links[\u0026#39;annuler\u0026#39;] = {\u0026#39;href\u0026#39;: base, \u0026#39;method\u0026#39;: \u0026#39;DELETE\u0026#39;} if obj.status == ContratStatus.VALIDATED: links[\u0026#39;resilier\u0026#39;] = {\u0026#39;href\u0026#39;: f\u0026#39;{base}resilier/\u0026#39;, \u0026#39;method\u0026#39;: \u0026#39;POST\u0026#39;} if obj.client_id: links[\u0026#39;client\u0026#39;] = {\u0026#39;href\u0026#39;: f\u0026#39;/api/clients/{obj.client_id}/\u0026#39;, \u0026#39;method\u0026#39;: \u0026#39;GET\u0026#39;} return links Modelo de madurez Richardson: ¿dónde está tu API REST? Nivel Descripción Ejemplo 0 Un único endpoint POST SOAP, XML-RPC 1 Recursos distintos GET /contrats/1 2 Verbos HTTP correctos POST /contrats/ 3 HATEOAS Respuestas con _links ¿Hay que implementar HATEOAS siempre? No. Tiene sentido cuando la API es pública, el flujo de trabajo es complejo o se quiere reducir el acoplamiento. Para una API interna, los niveles 1-2 suelen ser suficientes.\n¿Trabajas en optimización de consultas Django? Echa un vistazo a Django in_bulk(): por qué es mejor que filter() en masa.\n","permalink":"https://dev-flow.io/es/posts/api-rest-hateoas/","summary":"\u003cp\u003eEn los equipos se escucha con frecuencia \u0026ldquo;tenemos una API REST\u0026rdquo;. Pero al revisar las respuestas JSON, no hay ningún enlace. Solo datos en crudo. Eso no es REST, es CRUD expuesto en HTTP.\u003c/p\u003e\n\u003cp\u003eLa diferencia la marca un principio que la mayoría de los desarrolladores ignora: \u003cstrong\u003eHATEOAS\u003c/strong\u003e.\u003c/p\u003e\n\u003ch2 id=\"qué-es-hateoas-en-una-api-rest\"\u003e¿Qué es HATEOAS en una API REST?\u003c/h2\u003e\n\u003cp\u003eHATEOAS significa \u003cstrong\u003eHypermedia As The Engine Of Application State\u003c/strong\u003e. Es una de las restricciones fundamentales del REST, definida por Roy Fielding en su tesis del año 2000.\u003c/p\u003e","title":"HATEOAS: tu API REST podría ser solo CRUD"},{"content":"Con el ORM de Django hay dos formas de añadir un valor calculado sobre un conjunto de filas: annotate() con una agregación clásica (Max, Count, Sum\u0026hellip;) o annotate() con una Window function. A simple vista se parecen. En la práctica, su comportamiento es fundamentalmente distinto — y elegir la opción equivocada puede bloquear toda la cadena de filtrado.\nGROUP BY con annotate(): filas que se comprimen Cuando combinamos values() y annotate() con una agregación, Django genera un GROUP BY en SQL. El resultado: las filas se agrupan y obtenemos una fila por grupo.\nfrom django.db.models import Max def get_latest_dates(self) -\u0026gt; QuerySet: return self.values(\u0026#39;ctr_id\u0026#39;).annotate( latest_date=Max(\u0026#39;evt_end_effect_date\u0026#39;) ) SQL generado:\nSELECT ctr_id, MAX(evt_end_effect_date) AS latest_date FROM events GROUP BY ctr_id El resultado es un diccionario por grupo {'ctr_id': 1, 'latest_date': date(2024, 12, 31)} — ya no hay instancias de modelo completas, solo los campos agregados.\nLo que hay que entender sobre el encadenamiento: aún es posible llamar a .filter() o .exclude() después, pero la semántica cambia radicalmente. Los filtros se aplican sobre los grupos agregados, no sobre las filas originales. Ya no filtramos eventos individuales, filtramos resultados de grupo.\n# ⚠️ Este filter se aplica sobre los grupos, no sobre las filas fuente self.values(\u0026#39;ctr_id\u0026#39;).annotate(latest_date=Max(\u0026#39;evt_end_effect_date\u0026#39;)).filter( latest_date__gte=date(2024, 1, 1) ) # SQL: HAVING MAX(evt_end_effect_date) \u0026gt;= \u0026#39;2024-01-01\u0026#39; # Sin select_related(), sin acceso a los demás campos de la fila Window functions: anotar sin tocar las filas Una Window function calcula un valor sobre una partición de filas, pero conserva todas las filas intactas. Cada fila recibe su valor calculado como una anotación adicional.\nfrom django.db.models import F, Window from django.db.models.functions import FirstValue def with_latest_dates(self) -\u0026gt; QuerySet: return self.annotate( latest_date=Window( expression=FirstValue(\u0026#39;evt_end_effect_date\u0026#39;), partition_by=[\u0026#39;ctr_id\u0026#39;], order_by=F(\u0026#39;evt_end_effect_date\u0026#39;).desc(), ) ) SQL generado:\nSELECT *, FIRST_VALUE(evt_end_effect_date) OVER ( PARTITION BY ctr_id ORDER BY evt_end_effect_date DESC ) AS latest_date FROM events Todas las filas están presentes. Cada una tiene ahora latest_date — la fecha más reciente de su grupo ctr_id. Y el QuerySet sigue siendo un QuerySet normal.\n# ✅ Todo sigue siendo posible tras una anotación Window qs = self.with_latest_dates() qs.filter(status=\u0026#39;active\u0026#39;) # filtro normal (WHERE sobre columna no-window) qs.select_related(\u0026#39;contract\u0026#39;) # join normal qs.exclude(latest_date__isnull=True) # filtro sobre la anotación window -\u0026gt; subconsulta qs.order_by(\u0026#39;ctr_id\u0026#39;, \u0026#39;-latest_date\u0026#39;) Django GROUP BY vs Window Function: comparación visual GROUP BY Window function ────────────────────────────────── ────────────────────────────────── ctr_id=1, evt=A → fila 1 ctr_id=1, evt=A, latest=A → fila 1 ctr_id=1, evt=B → ctr_id=1, evt=B, latest=A → fila 2 ctr_id=1, evt=C → MAX(C) ──► C ctr_id=1, evt=C, latest=A → fila 3 ctr_id=2, evt=D → fila 2 ctr_id=2, evt=D, latest=D → fila 4 ctr_id=2, evt=E → MAX(E) ──► E ctr_id=2, evt=E, latest=D → fila 5 GROUP BY comprime. Window anota.\nCaso de uso concreto: recuperar la fila más reciente por grupo Objetivo: para cada contrato, recuperar el evento con la fecha de fin de efecto más reciente, con acceso a todos sus campos.\nCon GROUP BY es imposible de forma directa — se pierden los campos de la fila. Con Window + RowNumber:\nfrom django.db.models import F, Window from django.db.models.functions import RowNumber def get_latest_event_per_contract(self) -\u0026gt; QuerySet: return ( self.annotate( row_num=Window( expression=RowNumber(), partition_by=[\u0026#39;ctr_id\u0026#39;], order_by=F(\u0026#39;evt_end_effect_date\u0026#39;).desc(), ) ) .filter(row_num=1) .select_related(\u0026#39;contract\u0026#39;) ) RowNumber() numera las filas dentro de cada partición, ordenadas por fecha descendente. filter(row_num=1) conserva únicamente la primera — la más reciente. Django no puede añadir un WHERE directo sobre una Window function (imposible en SQL estándar), así que genera una subconsulta: SELECT * FROM (...) \u0026quot;qualify\u0026quot; WHERE \u0026quot;row_num\u0026quot; = 1.\nWindow functions disponibles en Django from django.db.models.functions import ( FirstValue, # primer valor de la partición LastValue, # último valor Lag, # valor N filas atrás: Lag(\u0026#39;field\u0026#39;, offset=1) Lead, # valor N filas adelante: Lead(\u0026#39;field\u0026#39;, offset=1) NthValue, # enésimo valor: NthValue(\u0026#39;field\u0026#39;, nth=2) Rank, # rango con empates (1, 1, 3) DenseRank, # rango sin saltos (1, 1, 2) RowNumber, # número de fila único por partición CumeDist, # distribución acumulada (0.0 → 1.0) PercentRank, # rango relativo (0.0 → 1.0) Ntile, # división en N buckets: Ntile(num_buckets=4) ) GROUP BY o Django Window Function: tabla de decisión values().annotate(Max(...)) annotate(Window(...)) SQL GROUP BY OVER (PARTITION BY ...) Filas conservadas Una por grupo Todas Acceso a los campos Solo los incluidos en values() Todos Encadenabilidad Filtro sobre grupos (HAVING) Filtro vía subconsulta (WHERE sobre las filas) select_related() ❌ ✅ Caso de uso Contar, sumar, máximo global Rango, valor vecino, máximo por fila La regla sencilla: si necesitas conservar las filas completas y seguir filtrando con normalidad después del cálculo, usa una Window function. Si solo quieres resultados agregados (estadísticas, totales, máximos globales), values().annotate() es más directo y más legible.\n¿Trabajas en la optimización del ORM de Django? También escribí sobre Django in_bulk(): por qué es mejor que filter() en masa.\n","permalink":"https://dev-flow.io/es/posts/django-window-group-by/","summary":"\u003cp\u003eCon el ORM de Django hay dos formas de añadir un valor calculado sobre un conjunto de filas: \u003ccode\u003eannotate()\u003c/code\u003e con una agregación clásica (\u003ccode\u003eMax\u003c/code\u003e, \u003ccode\u003eCount\u003c/code\u003e, \u003ccode\u003eSum\u003c/code\u003e\u0026hellip;) o \u003ccode\u003eannotate()\u003c/code\u003e con una \u003cstrong\u003eWindow function\u003c/strong\u003e. A simple vista se parecen. En la práctica, su comportamiento es fundamentalmente distinto — y elegir la opción equivocada puede bloquear toda la cadena de filtrado.\u003c/p\u003e\n\u003ch2 id=\"group-by-con-annotate-filas-que-se-comprimen\"\u003eGROUP BY con annotate(): filas que se comprimen\u003c/h2\u003e\n\u003cp\u003eCuando combinamos \u003ccode\u003evalues()\u003c/code\u003e y \u003ccode\u003eannotate()\u003c/code\u003e con una agregación, Django genera un \u003ccode\u003eGROUP BY\u003c/code\u003e en SQL. El resultado: las filas se agrupan y obtenemos \u003cstrong\u003euna fila por grupo\u003c/strong\u003e.\u003c/p\u003e","title":"Django Window Function vs GROUP BY: QuerySets encadenables"},{"content":"Cuando tenemos una lista de identificadores y queremos recuperar las instancias correspondientes, el reflejo habitual en Django es filter(pk__in=[...]). Funciona — una sola consulta SQL. Pero in_bulk() es una optimización ORM frecuentemente ignorada: retorna un diccionario {id: instancia} en lugar de un QuerySet, lo que cambia radicalmente la forma de acceder a los resultados. Donde filter() obliga a un recorrido O(n) para encontrar un objeto por su ID, in_bulk() da acceso directo O(1).\nFirma de in_bulk() y comportamiento QuerySet.in_bulk(id_list=(), *, field_name=\u0026#39;pk\u0026#39;) id_list: lista de identificadores a recuperar. Si se omite (llamado sin argumentos), retorna todos los objetos de la tabla. field_name: campo usado como clave del diccionario. Debe tener unique=True, de lo contrario Django lanza un ValueError. La consulta SQL generada es una simple cláusula WHERE pk IN (...) — una sola consulta independientemente del tamaño de la lista.\nin_bulk() vs filter(): acceso O(1) en lugar de O(n) # filter() → QuerySet, acceso O(n) contratos: list[Contrato] = list(Contrato.objects.filter(pk__in=[1, 2, 3])) contrato: Contrato | None = next((c for c in contratos if c.pk == 2), None) # in_bulk() → dict, acceso O(1) contratos_map: dict[int, Contrato] = Contrato.objects.in_bulk([1, 2, 3]) # → {1: \u0026lt;Contrato pk=1\u0026gt;, 2: \u0026lt;Contrato pk=2\u0026gt;, 3: \u0026lt;Contrato pk=3\u0026gt;} contrato = contratos_map.get(2) # acceso directo, None si ausente Los IDs ausentes en base de datos simplemente no aparecen en el diccionario retornado. Sin error, sin None: clave ausente = objeto inexistente.\nin_bulk() con field_name: indexar por cualquier campo único in_bulk() acepta cualquier campo unique=True mediante field_name:\n# Por referencia única refs: list[str] = [\u0026#39;REF-001\u0026#39;, \u0026#39;REF-002\u0026#39;, \u0026#39;REF-003\u0026#39;] contratos_map: dict[str, Contrato] = Contrato.objects.in_bulk( refs, field_name=\u0026#39;referencia\u0026#39; ) # → {\u0026#39;REF-001\u0026#39;: \u0026lt;Contrato ...\u0026gt;, \u0026#39;REF-002\u0026#39;: \u0026lt;Contrato ...\u0026gt;, ...} contrato: Contrato | None = contratos_map.get(\u0026#39;REF-002\u0026#39;) Especialmente útil en sincronizaciones de datos donde el identificador de negocio no es la PK.\nCasos de uso Django: cuándo in_bulk() marca la diferencia Hidratar varios agregados en una consulta En un contexto DDD, cuando hay que cargar varios agregados a partir de una lista de IDs:\nids: list[int] = [evento.contrato_id for evento in eventos] contratos_map: dict[int, Contrato] = Contrato.objects.in_bulk(ids) for evento in eventos: contrato: Contrato | None = contratos_map.get(evento.contrato_id) if contrato: contrato.aplicar(evento) Una sola consulta para todos los contratos, luego acceso directo por ID en el bucle.\nEvitar el N+1 durante imports from decimal import Decimal def importar_filas(filas_csv: list[dict[str, str]]) -\u0026gt; None: referencias: list[str] = [fila[\u0026#39;ref\u0026#39;] for fila in filas_csv] existentes: dict[str, Producto] = Producto.objects.in_bulk( referencias, field_name=\u0026#39;referencia\u0026#39; ) a_crear: list[Producto] = [] a_actualizar: list[Producto] = [] for fila in filas_csv: if fila[\u0026#39;ref\u0026#39;] in existentes: producto = existentes[fila[\u0026#39;ref\u0026#39;]] producto.precio = Decimal(fila[\u0026#39;precio\u0026#39;]) a_actualizar.append(producto) else: a_crear.append(Producto(referencia=fila[\u0026#39;ref\u0026#39;], precio=fila[\u0026#39;precio\u0026#39;])) Producto.objects.bulk_create(a_crear) Producto.objects.bulk_update(a_actualizar, [\u0026#39;precio\u0026#39;]) Patrón clásico import/sincronización: una consulta in_bulk(), luego bulk_create + bulk_update. Cero N+1.\nRecuperar todos los objetos de una tabla # Carga toda la tabla en memoria — reservar para tablas pequeñas config: dict[int, ParametroApp] = ParametroApp.objects.in_bulk() valor: str = config[42].valor Práctico para tablas de referencia (países, divisas, parámetros) consultadas frecuentemente.\nOptimizar in_bulk() en listas grandes con chunking Para listas de miles de IDs, la cláusula IN(...) puede volverse pesada para la base de datos. La solución: dividir en lotes.\nfrom collections.abc import Iterator from itertools import islice from typing import Any from django.db.models import QuerySet def chunked(iterable: list[Any], size: int) -\u0026gt; Iterator[list[Any]]: it = iter(iterable) while chunk := list(islice(it, size)): yield chunk def in_bulk_chunked( queryset: QuerySet, ids: list[Any], chunk_size: int = 500, field_name: str = \u0026#39;pk\u0026#39;, ) -\u0026gt; dict[Any, Any]: result: dict[Any, Any] = {} for chunk in chunked(ids, chunk_size): result.update(queryset.in_bulk(chunk, field_name=field_name)) return result # Uso contratos: dict[int, Contrato] = in_bulk_chunked( Contrato.objects, lista_de_5000_ids ) Resumen: in_bulk() vs filter() en Django filter(pk__in=[...]) in_bulk([...]) Retorno QuerySet (lista) dict {id: instancia} Acceso por ID O(n) — recorrido O(1) — clave directa Consultas SQL 1 1 IDs ausentes ignorados silenciosamente clave ausente del dict field_name no sí (unique=True requerido) in_bulk() no es un reemplazo universal de filter(). Es una herramienta específica: cuando se tienen IDs y se quiere acceso directo por clave, es la elección correcta. Para todo lo demás, filter() sigue siendo perfectamente adecuado.\n¿Trabajas en temas de rendimiento Django? Echa un vistazo a por qué la IA hace que aprender a programar sea más esencial que nunca.\n","permalink":"https://dev-flow.io/es/posts/django-in-bulk/","summary":"\u003cp\u003eCuando tenemos una lista de identificadores y queremos recuperar las instancias correspondientes, el reflejo habitual en Django es \u003ccode\u003efilter(pk__in=[...])\u003c/code\u003e. Funciona — una sola consulta SQL. Pero \u003ccode\u003ein_bulk()\u003c/code\u003e es una optimización ORM frecuentemente ignorada: retorna un \u003cstrong\u003ediccionario\u003c/strong\u003e \u003ccode\u003e{id: instancia}\u003c/code\u003e en lugar de un QuerySet, lo que cambia radicalmente la forma de acceder a los resultados. Donde \u003ccode\u003efilter()\u003c/code\u003e obliga a un recorrido O(n) para encontrar un objeto por su ID, \u003ccode\u003ein_bulk()\u003c/code\u003e da acceso directo O(1).\u003c/p\u003e","title":"Django in_bulk(): por qué es mejor que filter() en masa"},{"content":"Últimamente escuchamos la misma promesa una y otra vez: \u0026ldquo;Ya no hace falta saber programar, la IA se encarga.\u0026rdquo; Y francamente, es tentador. Abres un agente, describes lo que quieres, y en segundos aparece el código. Magia.\nExcepto que no. No exactamente.\nEl desarrollo agéntico, ¿pero para quién? El desarrollo asistido por IA es una revolución real. No voy a fingir lo contrario. Para un desarrollador senior o intermedio que ya se ha enfrentado a problemas complejos, depurado algoritmos retorcidos y puesto sistemas en producción, la productividad alcanza niveles sin precedentes. Delegas las tareas repetitivas, prototipas en horas lo que antes llevaba días, y te mantienes en tu zona de alto valor: la arquitectura, las decisiones críticas, la validación.\nPero hay una palabra clave en esa frase: validación.\nPorque el desarrollo agéntico está lejos de ser perfecto. El agente alucina. Genera código que compila pero que es fundamentalmente incorrecto. Ignora las convenciones del proyecto, elude las buenas prácticas e introduce vulnerabilidades de seguridad de forma silenciosa. Para aprovechar realmente esta herramienta, hay que poder guiarla, corregirla, cuestionarla.\nY para guiar a un agente de código, hay que saber programar.\nValidar sin entender: una ilusión peligrosa Imagina poner a cargo de la supervisión de una obra a alguien que nunca ha pisado un andamio. Podrá comprobar si las paredes están rectas, si la pintura queda bien. ¿Pero los cimientos? ¿Las normas antisísmicas? ¿La conformidad eléctrica? Eso se le escapará por completo.\nEsto es exactamente lo que ocurre cuando un desarrollador sin experiencia práctica intenta validar código generado por una IA. Puede verificar que \u0026ldquo;funciona\u0026rdquo; en la superficie. Pero la legibilidad, el mantenimiento, el manejo de casos límite, la corrección algorítmica, los riesgos de seguridad: todo eso le resultará invisible.\nLa validación será superficial. Y en nuestro oficio, lo superficial siempre explota en producción.\nEl aprendizaje no puede ser solo teórico Lo sabemos: aprender a desarrollar requiere práctica. Escribir líneas de código. Equivocarse. Depurar durante tres horas para descubrir que faltaba una coma. Implementar un algoritmo desde cero para entender por qué importa la complejidad temporal. Luchar con una regresión inexplicable hasta desarrollar un instinto para las causas probables.\nEsas son las cicatrices que forman a un desarrollador capaz de juzgar, anticipar y decidir.\nPero si la IA está siempre ahí para \u0026ldquo;rescatar\u0026rdquo; al aprendiz de cada obstáculo, la pereza cognitiva se instala. ¿Para qué pensar si la IA responde? ¿Para qué explorar si la solución está a un prompt de distancia? Dejas de enfrentarte al problema. Delegas el pensamiento. Y sin darte cuenta, nunca desarrollas los reflejos que marcan la diferencia.\nLa pregunta que nadie hace lo suficiente todavía En cinco a diez años, una generación de seniors se jubilará. Esos desarrolladores que construyeron sistemas críticos, que conocen los patrones probados, que pueden decir \u0026ldquo;ya he visto esto salir mal\u0026rdquo;\u0026hellip; van a desaparecer.\n¿Quién los reemplazará?\n¿Desarrolladores formados en un mundo donde la IA escribe el código por ellos, donde el aprendizaje práctico fue cortocircuitado por la comodidad? ¿Personas capaces de escribir buenos prompts, pero incapaces de auditar con rigor lo que el agente produjo?\nHoy, el código crítico en producción todavía lo validan humanos competentes. Pero esa competencia no es hereditaria. Se construye, con dificultad, a través de la experiencia.\n¿Y si la IA se volviera perfecta mañana? Puede ser. Los avances son reales y se aceleran. Es posible que algún día los agentes sean capaces de autovalidación cualitativa: verificar por sí mismos que el código que producen sigue las buenas prácticas, es seguro, eficiente y mantenible.\nPero en mi experiencia, incluso hoy con los modelos más avanzados, si quieres código limpio, tienes que guiarlo. Darle contexto. Imponerle restricciones. Corregir su rumbo. Y para hacer todo eso, necesitas visión. Experiencia. Criterio.\nLa IA es una herramienta extraordinaria. Pero como toda herramienta, su eficacia depende enteramente de la mano que la sostiene.\nConclusión: aprender a programar nunca ha sido tan importante Paradójicamente, el auge de la IA en el desarrollo hace que aprender a programar sea más esencial, no menos. No para escribir cada línea uno mismo — esa es una visión del pasado — sino para conservar la capacidad de comprender, evaluar y orientar lo que producen las máquinas.\nLos nuevos desarrolladores que inviertan en este aprendizaje difícil y práctico serán quienes saquen más partido de la IA. Los demás serán, en el mejor de los casos, operadores de superficie: competentes cuando todo va bien, perdidos en cuanto algo falla.\nEl código todavía se aprende. Y se aprende viviéndolo.\n¿Acabas de llegar a DevFlow? Descubre por qué este blog habla de Python, Django y FastAPI.\n","permalink":"https://dev-flow.io/es/posts/ia-aprendizaje-codigo/","summary":"\u003cp\u003eÚltimamente escuchamos la misma promesa una y otra vez: \u003cem\u003e\u0026ldquo;Ya no hace falta saber programar, la IA se encarga.\u0026rdquo;\u003c/em\u003e Y francamente, es tentador. Abres un agente, describes lo que quieres, y en segundos aparece el código. Magia.\u003c/p\u003e\n\u003cp\u003eExcepto que no. No exactamente.\u003c/p\u003e\n\u003ch2 id=\"el-desarrollo-agéntico-pero-para-quién\"\u003eEl desarrollo agéntico, ¿pero para quién?\u003c/h2\u003e\n\u003cp\u003eEl desarrollo asistido por IA es una revolución real. No voy a fingir lo contrario. Para un desarrollador senior o intermedio que ya se ha enfrentado a problemas complejos, depurado algoritmos retorcidos y puesto sistemas en producción, la productividad alcanza niveles sin precedentes. Delegas las tareas repetitivas, prototipas en horas lo que antes llevaba días, y te mantienes en tu zona de alto valor: la arquitectura, las decisiones críticas, la validación.\u003c/p\u003e","title":"La IA no reemplaza aprender a programar"},{"content":"Este blog es ante todo un espacio de intercambio: descubrimientos, reflexiones, cosas que me han sido útiles y que podrían serlo para otros.\nPython, Django, FastAPI y DRF: el núcleo del blog El núcleo del blog es el desarrollo en Python, y más concretamente los frameworks que estructuran mi día a día: Django, FastAPI y Flask. Cada uno tiene sus puntos fuertes, sus casos de uso, sus trampas. Profundizaremos en todos ellos.\nPero más allá del código que funciona, lo que me interesa es el código que dura. Por eso también hablaremos de métodos y prácticas:\nTDD: escribir los tests antes del código, y por qué cambia realmente la forma de pensar SOLID: los principios detrás del código mantenible DDD: modelar el negocio, no solo la base de datos Y luego está todo lo que se acumula con la experiencia: los patrones que uno adopta, los que abandona, los errores que se dejan de cometer, los atajos que se aprende a evitar.\nGo, Lua, JavaScript y otros lenguajes De vez en cuando, saldremos del perímetro. Go por lo que aporta en términos de rendimiento y simplicidad en ciertos contextos. Lua por sus casos de uso inesperados. Otros lenguajes cuando surja la ocasión, no por exhaustividad, sino por curiosidad.\nBienvenido a DevFlow.\n¿Te interesa el desarrollo en la era de la IA? Lee mi artículo sobre por qué la IA hace que aprender a programar sea más esencial que nunca.\n","permalink":"https://dev-flow.io/es/posts/por-que-este-blog/","summary":"\u003cp\u003eEste blog es ante todo un espacio de intercambio: descubrimientos, reflexiones, cosas que me han sido útiles y que podrían serlo para otros.\u003c/p\u003e\n\u003ch2 id=\"python-django-fastapi-y-drf-el-núcleo-del-blog\"\u003ePython, Django, FastAPI y DRF: el núcleo del blog\u003c/h2\u003e\n\u003cp\u003eEl núcleo del blog es el desarrollo en \u003cstrong\u003ePython\u003c/strong\u003e, y más concretamente los frameworks que estructuran mi día a día: \u003cstrong\u003eDjango\u003c/strong\u003e, \u003cstrong\u003eFastAPI\u003c/strong\u003e y \u003cstrong\u003eFlask\u003c/strong\u003e. Cada uno tiene sus puntos fuertes, sus casos de uso, sus trampas. Profundizaremos en todos ellos.\u003c/p\u003e","title":"¿Por qué un blog sobre Python, Django y FastAPI?"}]