Especializado en Angular moderno, TypeScript, RxJS, Signals y NgRx, con experiencia
construyendo aplicaciones web reales, SSR, arquitectura frontend, rendimiento e
integración end-to-end.
Soy Angular Frontend Engineer especializado en Angular moderno,
TypeScript, RxJS, Signals y NgRx, con experiencia construyendo aplicaciones web
reales desde la arquitectura frontend hasta producción.
Mi foco principal es la arquitectura mantenible, el estado reactivo, SSR,
rendimiento, integración con APIs, autenticación, accesibilidad y experiencia de
usuario. También trabajo con NestJS, GraphQL, PostgreSQL, Docker y AWS, lo que me
permite entender el producto end-to-end sin diluir mi especialización frontend.
Alan Buscaglia (ConquerBlocks Academy - Gentleman Programming)
Arquitectura Frontend con Angular - Buenas Prácticas - Clean Architecture
Evolución hacia la lógica estructural, Clean Architecture y adopción de Angular con tipado estricto como framework principal. También me introdujo a Engram y a una metodología de trabajo con agentes de IA.
Construcción de cimientos técnicos robustos y adopción de un enfoque pragmático ante retos complejos. También me introdujo a la semántica y a la importancia del SEO en el desarrollo de aplicaciones web.
Metodología Base - Lógica de Programación - Estructuras de control
Asentamiento de la lógica de programación y el rigor algorítmico utilizando Python. Más que sintaxis, esta etapa forjó una disciplina metodológica esencial para aprender a abstraer problemas complejos y estructurar el pensamiento antes de escribir código en un lenguaje de programación.
Su mentoría en Upgrade-hub no solo me impulsó a desarrollar interfaces interactivas, sino que sentó las bases de mis aptitudes de organización de código y el uso profesional de Git para el control de versiones.
Aquí recopilo algunos de los fallos reales de los que he aprendido al diagnosticar y resolver problemas en producción,
y que me recuerdan por qué nunca voy a dejar de aprender dentro de este maravilloso mundo del software.
Cada caso muestra un titular descriptivo y un resumen del incidente. Con
Ver diagnóstico se abre el detalle: contexto, error, investigación,
solución y aprendizaje. También puedes usar el buscador.
Angular 21Open GraphSEOVercelCSR
WhatsApp no mostraba el logo: los metadatos de Angular no estaban en el HTML inicial
Los scrapers sociales no ejecutaban el JavaScript de Angular; añadí un fallback Open Graph estático y pospuse las tarjetas específicas por ruta hasta SSR o prerenderizado.
Ver diagnóstico →Ocultar diagnóstico ↑
Contexto
Tras completar la Fase A de SEO de Finca El Sarao, la aplicación Angular CSR ya estaba desplegada en Vercel. El siguiente objetivo era conseguir que los enlaces compartidos en WhatsApp y otras redes generasen una vista previa corporativa reconocible, con una imagen Open Graph de 1200 × 630 píxeles, el logo de la marca y metadatos coherentes, sin introducir todavía SSR ni prerenderizado.
Error / Dificultad
$
La imagen Open Graph por defecto seguía apuntando a hero-cover.jpg, una fotografía de paisaje sin identidad corporativa. Aunque Angular actualizaba los metadatos al navegar, los scrapers sociales recibían únicamente el HTML inicial y no ejecutaban JavaScript, por lo que nunca veían esos cambios. Además, definir un og:url o un canonical fijo en index.html habría atribuido incorrectamente la misma URL a todas las rutas profundas de la aplicación.
Investigación
Comparé los metadatos que mostraba el DOM después de arrancar Angular con los incluidos en la respuesta HTML original. El problema no era la disponibilidad de la imagen ni el funcionamiento del SeoService: Vercel entregaba el mismo index.html para todas las rutas y los scrapers sociales no ejecutaban el JavaScript encargado de modificarlo. También descarté construir la URL de la imagen con window.location.origin, porque el crawler necesita encontrar una URL absoluta antes de que la aplicación se ejecute.
Solución
Creé una tarjeta Open Graph corporativa de 1200 × 630 píxeles y dejé en index.html los metadatos invariantes que debían estar disponibles sin JavaScript. Convertí DEFAULT_OG_IMAGE en una URL absoluta, mantuve SITE_LOGO como recurso independiente para los datos estructurados y centralicé los metadatos variables de cada vista mediante SeoService.setPageMeta(). Evité declarar valores globales engañosos para og:url y canonical. La implementación y sus ajustes quedaron integrados en los PR #60 y #61.
Aprendizaje
En una aplicación CSR, lo que no está en el HTML inicial no existe para un scraper sin JavaScript. Una tarjeta social estática puede ser fiable; si cada URL necesita una vista previa propia, la solución no es añadir más JavaScript, sino generar HTML por ruta mediante SSR o prerenderizado.
Vercel bloqueaba el despliegue de Angular por errores de tipos de NestJS
El pipeline de Vercel fallaba tras un ng build correcto porque typechequeaba NestJS/Prisma del monorepo; fijé Root Directory en web con Include outside enabled.
Ver diagnóstico →Ocultar diagnóstico ↑
Contexto
Estaba cerrando el deploy de Finca El Sarao (monorepo pnpm: Angular 21 en web/, NestJS 11 + Prisma 7 en api/) tras la Fase A SEO. El frontend va a Vercel (fincaelsarao.com); la API y PostgreSQL corren en Railway (api.fincaelsarao.com). No hay proxy same-origin como en GTVMOTOR: la SPA llama cross-origin con cookies HttpOnly para refresh. El build local y ng build en Vercel pasaban; el pipeline fallaba en fases posteriores o al cambiar Root Directory.
Error / Dificultad
$
Cadena de fallos en CI de Vercel: (1) `Command prisma:generate not found` al ejecutar scripts desde cwd incorrecto. (2) Con Root Directory = `./`, `ng build` terminaba bien pero después aparecían decenas de ciclos `Using TypeScript 5.9.3` y `TS2305: Module @prisma/client has no exported member UserRole` en `api/src/...`. Prisma sí generaba el cliente en postinstall. (3) Tras mover Root a `web` y desactivar «Include files outside the root directory», el install falló con `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND` en `/vercel` porque `cd ..` / `pnpm --dir ..` no veía el `package.json` raíz del monorepo.
Investigación
El log mostraba que el error TS2305 ocurría **después** de Application bundle generation complete, no durante vercel-build. Eso apunta a la fase de Serverless Functions de Vercel, no al script de build del repo. Con Root = ./, Vercel reserva la carpeta api/ para compilar NestJS como functions aunque el buildCommand solo ejecute Prisma + Angular. Con Root = web, ../api queda fuera del proyecto Vercel (solo reservaría web/api/, que no existe). Pero un monorepo pnpm necesita el lockfile y workspaces en la raíz: sin «Include outside» el sandbox solo monta web/ y --dir .. apunta a /vercel vacío. Documenté la cronología en docs/DEPLOY.md y contrasté con GTVMOTOR (Nginx same-origin) vs Finca (cross-origin + rewrite único de /sitemap.xml).
Solución
Config validada en main (ee3c489): Root Directory = web, «Include files outside…» = **Enabled**, overrides del dashboard vacíos. web/vercel.json: installCommand: HUSKY=0 pnpm install --dir .., buildCommand: pnpm run vercel-build. web/package.json delega: vercel-build → pnpm --dir .. run vercel-build. Script raíz: pnpm --filter api exec prisma generate && pnpm --filter web run build (sin nest build). .npmrc: node-linker=hoisted + hoist de @prisma/client. api/package.json: postinstall: prisma generate por si Vercel aísla installs sobre api/. Build ~1 min, deploy OK en fincaelsarao.com.
Aprendizaje
En monorepos en Vercel, Root Directory y «Include outside» son dos ejes independientes: el primero evita que carpetas hermanas (api/) se traten como serverless; el segundo permite que el install vea el package.json raíz. Un ng build verde no garantiza deploy verde si la plataforma compila más código después. Si Prisma generate pasa pero TS2305 aparece en api/, sospecha de Serverless Functions, no de schema. Para SPA + API en otro host, documentar cross-origin (CORS + withCredentials) y no asumir que la receta same-origin de un proyecto anterior aplica tal cual.
AstroTypeScriptFuse.jsDOM
Fuse.js mostraba entradas sin resaltar: el filtro fuzzy no coincidía con el DOM
El buscador devolvía coincidencias aproximadas sin poder resaltarlas en el DOM; prioricé la coincidencia literal y limité el resaltado fuzzy a los matches reales de Fuse.
Ver diagnóstico →Ocultar diagnóstico ↑
Contexto
El buscador de la bitácora (<logbook-search>) ya filtraba con Fuse.js, pero al añadir resaltado literal en el DOM apareció un fallo de confianza: entradas visibles sin la palabra buscada marcada en ningún sitio. Quería que cada resultado fuera comprobable (solo mostrar lo que se puede resaltar, contador honesto, <details> abiertos con sentido), aunque eso vaya en contra de lo que Fuse hace por defecto: tolerar similitudes sin exigir el texto exacto.
Error / Dificultad
$
Con Fuse.js como único filtro (`threshold: 0.35`), búsquedas como `inje` abrían entradas sin ningún resaltado visible. Fuse devolvía similitudes difusas; el `<mark>` solo pintaba subcadenas literales del input. El contador decía «4 veces · 4 entradas» pero tres artículos no mostraban la palabra buscada. Al pasar a solo DOM literal se corrigió el falso positivo, pero se perdía la tolerancia a typos y la relevancia fuzzy que justificaba mantener Fuse en `package.json`.
Investigación
Se evaluaron tres caminos: (1) eliminar Fuse y quedarse con substring en el DOM, simple pero sin aproximaciones; (2) volver a fuzzy puro alineando resaltado con includeMatches de Fuse, sin mostrar el query literal cuando no existe; (3) híbrido: prioridad a coincidencia exacta en el DOM, fallback fuzzy solo si Fuse encuentra la entrada y el DOM puede resaltar los spans de result.matches. También hizo falta alinear índice y DOM: research y learning entraron al JSON, y cada sección larga del caso lleva un identificador en el HTML para que el resaltado fuzzy caiga en el bloque correcto (investigación, solución o aprendizaje), no en cualquier párrafo.
Solución
Lo quiero todo, adapatación al modelo híbrido. En LogbookSearch.astro, cada entrada pasa primero por #highlightLiteral(query) (teal). Si hits === 0, se consulta Fuse con includeMatches: true y #highlightFuseMatches pinta en ámbar el fragmento que Fuse emparejó, no el texto del input. Las aproximadas llevan badge ~aprox en el summary. Si Fuse sugiere una entrada pero el DOM no puede marcar nada, se oculta. El contador desglosa exactas y aprox (2 exactas · 1 aprox). LogbookSection.astro serializa el índice completo; en LogbookEntry.astro, cada sección larga del caso lleva un identificador en el HTML para que el resaltado fuzzy caiga en el bloque correcto (investigación, solución o aprendizaje), no en cualquier párrafo.
Aprendizaje
Filtro y resaltado deben compartir criterio o el usuario ve resultados fantasma. Un híbrido honesto: exacto primero (predecible), fuzzy como segunda capa con visual distinto (ámbar + ~aprox) y highlight de lo que Fuse encontró, no de lo que el usuario escribió. Mantener Fuse tiene sentido si la segunda capa está acoplada a matches; si no, es dependencia muerta. Para pocos artículos el literal basta; el híbrido escala mejor cuando crezca la bitácora o haya typos en errores de consola largos.
Angular lanzaba NG0200: SignalStore y Apollo crearon una dependencia circular
El bootstrap fallaba por el ciclo AppStore → HostPending → Apollo → AppStore; rompí la inyección eager con Injector y resolución perezosa en la acción.
Ver diagnóstico →Ocultar diagnóstico ↑
Contexto
En el front de Slowork, el sidebar del admin muestra un badge con el total de solicitudes pendientes (creadores vía REST y hosts vía GraphQL). Ese número vive en AppStore, un NgRx SignalStore global. Tras tocar la carga de contadores en el store, la aplicación dejó de arrancar en /login: antes de poder validar si el badge volvía a funcionar, el bootstrap ya fallaba con un error de dependencias.
Error / Dificultad
$
RuntimeError NG0200: dependencia circular detectada para `SignalStore`. Ruta: `_Auth` → `SignalStore` → `_HostPendingService` → `_Apollo` → `InjectionToken APOLLO_OPTIONS` → `SignalStore`. La app fallaba al cargar `/login` antes de que el sidebar pudiera pedir los contadores.
Investigación
Se trazó el ciclo completo en el grafo de DI. AppStore inyectaba HostPendingService en el bloque withMethods al **construir** el store. Ese servicio depende de Apollo. La factory de provideApollo (apollo.config.ts) a su vez hace inject(AppStore) para leer el token JWT y montar el authLink. Angular no puede instanciar dos tokens que se necesitan mutuamente durante la misma resolución. Se descartó mover la lógica a CollaborationReviewService (también inyecta AppStore). Como alternativa válida quedó usar HttpClient con POST GraphQL crudo, pero duplica la query ya definida en host-pending.queries.ts.
Solución
Inyección perezosa con Injector. En withMethods solo se resuelven dependencias seguras (Router, HttpClient, Injector). HostPendingService se obtiene **dentro** de loadAdminPendingCounts() con injector.get(HostPendingService), cuando el store ya está creado. La llamada a Hosts sigue siendo getPendingHosts(1, 1) y se guarda pageInfo.totalItems; Creadores usa REST con HttpClient. Ambas peticiones van en forkJoin con catchError(() => of(0)) para no romper el badge si una API falla.
Aprendizaje
Un store global no debe inyectar en su constructor servicios que dependan de infraestructura configurada con el propio store. El ciclo no aparece al ejecutar un método: aparece al **registrar** inject(HostPendingService) en withMethods. Regla práctica: estado central (token, usuario) ↔ cliente HTTP/GraphQL exige lazy resolution, un TokenService intermedio sin dependencias, o peticiones REST aisladas. En SignalStore, inject(Injector) + get() en la acción es el patrón mínimo para romper el ciclo sin duplicar queries.
Angular 21CSS ModernoSignals
La animación de salida del toast no se veía: Angular destruía el nodo antes del CSS
El @for eliminaba el toast al instante y cortaba la transición; usé @starting-style en la entrada y animate.leave para retener el nodo hasta el fin del CSS.
Ver diagnóstico →Ocultar diagnóstico ↑
Contexto
En el front de Slowork (Angular 21 zoneless) estaba puliendo las notificaciones toast del panel admin. Cada aviso se renderiza en un @for con temporizador de cierre: al expirar, Angular elimina el nodo al instante y la transición de salida no se llega a ver. El objetivo era animar entrada y salida con CSS nativo, sin añadir @angular/animations ni penalizar Lighthouse.
Error / Dificultad
$
Colisión de animaciones y destrucción prematura de nodos en el DOM. Al expirar el tiempo del Toast, Angular destruye el nodo instantáneamente por el flujo de control `@for`, impidiendo que las transiciones CSS de salida se ejecuten o provocando cortes abruptos en la interfaz gráfica.
Investigación
Análisis del ciclo de vida del DOM moderno y evaluación de alternativas. Cargar el paquete tradicional @angular/animations añade una penalización de rendimiento innecesaria en aplicaciones Zoneless y va en contra de la optimización del Core Web Vitals (Lighthouse 100/100). Históricamente se retenía el nodo con JS, pero los estándares web actuales permiten delegar la carga directamente al motor de renderizado del navegador.
Solución
Implementación de una estrategia híbrida nativa. Para la entrada, se utiliza la propiedad de CSS moderno @starting-style para definir los estilos del elemento antes de su primer renderizado en el navegador (solucionando la animación desde display: none). Para la salida, se utiliza el hook de animación nativo del framework (animate.leave), deteniendo la destrucción física del nodo en el DOM hasta que la transición CSS declare su finalización.
Aprendizaje
Menos JavaScript, más CSS nativo. Descargar la lógica de transiciones en las APIs modernas del navegador mantiene el hilo principal libre y la aplicación ultra fluida. La evolución de las herramientas modernas prioriza los estándares web sobre las abstracciones pesadas del framework. Además, se inmuniza el entorno de desarrollo actualizando las reglas globales de contexto (.cursor/rules / .mdc) para asegurar que todo el equipo y los agentes adopten esta solución nativa en futuros componentes interactivos (modales, menús, etc.), evitando deuda técnica.
AstroTypeScriptFuse.js
Fuse.js no encontraba resultados porque estaba indexando el objeto equivocado
JSON.stringify de CollectionEntry producía {}; serialicé objetos planos en el servidor antes de pasar el índice al Web Component.
Ver diagnóstico →Ocultar diagnóstico ↑
Contexto
Estaba montando el buscador de la bitácora en mi CV (Astro 6). Necesitaba filtrar entradas del timeline en el cliente sin React ni re-render del HTML del servidor: un Web Component con Fuse.js y un índice JSON generado en build. El primer paso era hacer llegar los datos de getCollection('logbook') al navegador de forma fiable.
Error / Dificultad
$
Al transportar el resultado de `getCollection('logbook')` al cliente, el índice de búsqueda llegaba vacío. Los objetos `CollectionEntry` de Astro contienen metadatos internos (símbolos, referencias a `.render()`) que no son JSON-serializables. `JSON.stringify` sobre la colección completa producía `{}` o fallaba en silencio, y Fuse.js no encontraba coincidencias aunque las entradas estuvieran en el DOM.
Investigación
Se revisó la documentación de Astro sobre el boundary servidor→cliente en islas y scripts de componente. La serialización de props con client:* exige datos planos; el mismo criterio aplica al pasar datos vía data-* attributes en un Custom Element. Se evaluó @astrojs/react con client:load, pero la interacción solo filtra nodos del DOM, así que no justifica el peso extra del runtime. Se optó por un HTMLElement con Fuse.js y manipulación directa de [data-entry-id].
Solución
En LogbookSection.astro se mapeó la colección a objetos planos antes de serializar: entries.map(e => ({ id: e.id, context: e.data.context, error: e.data.error, technologies: e.data.technologies, solution: e.data.solution })). El JSON viaja al cliente como data-index en <logbook-search>. El Web Component parsea el atributo en connectedCallback, inicializa Fuse.js y muestra u oculta los <li> del timeline con display: none, sin re-renderizar el HTML generado en el servidor.
Aprendizaje
En Astro, cualquier dato que cruce el boundary servidor→cliente, ya sea como prop de client:* o como atributo data-*, debe ser JSON puro. Los CollectionEntry hay que destruirlos a plain objects en el servidor. Para interacciones puntuales (filtrar, toggles, contadores), un Web Component en el <script> del .astro suele ser suficiente y evita dependencias de UI innecesarias.
Producción mostraba cero vehículos porque la API seguía conectada a localhost
El catálogo en prod salía vacío con la DB llena porque apiUrl apuntaba a localhost; unifiqué SPA y API bajo /api same-origin con Nginx.
Ver diagnóstico →Ocultar diagnóstico ↑
Contexto
Estaba cerrando el primer deploy en producción de GTVMOTOR en AWS EC2: Angular servido por Nginx en el host, API NestJS y PostgreSQL 16 en Docker, dominios gtvmotor.es y gtvmotor.co.uk con SSL. La infra de proxy se montó el 12/12/2025; al día siguiente, con todo desplegado, la web cargaba pero el catálogo de coches aparecía vacío para cualquier visitante.
Error / Dificultad
$
Tras el deploy, la web cargaba pero el inventario mostraba 0 vehículos pese a tener datos en PostgreSQL. `environment.prod.ts` seguía apuntando a `http://localhost:3000/api`: en el navegador del usuario esa URL no existe. Además, con varios dominios (.es, .co.uk, www) un front que llamara a otro host habría forzado CORS, preflight y cookies cruzadas. La API en Docker estaba bien acotada (`127.0.0.1:3000`), pero el cliente no pasaba por el reverse proxy de Nginx.
Investigación
Se depuró por capas desde la EC2: curl -s http://127.0.0.1:3000/api devolvía JSON con datos, mientras https://gtvmotor.es mostraba el SPA vacío. Se revisó el Security Group (solo 80/443 públicos, 3000 cerrado), el vhost en /etc/nginx/sites-available/gtvmotor y la diferencia entre /api y /api/ en proxy_pass. Se contrastó el enfoque Docker (nginx-proxy.conf con upstreams) con la estrategia final documentada en SPECIFICATION_PROD.md: Nginx en el host, estáticos en /var/www/gtvmotor/, proxy de /api/ hacia el contenedor local. Para SSL inicial se usó nginx-proxy.conf.temp-no-ssl hasta que Certbot validó los dominios.
Solución
Se unificó todo bajo same-origin. En environment.prod.ts: apiUrl: '/api' e imageUrl: '/api', de modo que el navegador siempre habla con gtvmotor.es y Nginx enruta internamente. Reglas de proxy: /api/ → 127.0.0.1:3000/api/, /docs y /docs-json al backend, / con try_files $uri $uri/ /index.html para el SPA. Redirect explícito location = /api { return 301 /api/; }. Dominio canónico gtvmotor.es con 301 desde www y .co.uk. API expuesta solo en loopback (127.0.0.1:3000:3000 en docker-compose). Certbot en Nginx y HSTS en el vhost HTTPS.
Aprendizaje
Un reverse proxy no es solo terminar SSL: es la fachada única que alinea SPA, API y documentación bajo un dominio. Si prod usa /api relativo, el front en dev debe tener un proxy equivalente o URLs coherentes; si no, funciona en un entorno y falla en otro. No exponer el puerto del backend: Nginx + binding localhost es defensa en profundidad. Los detalles de Nginx importan (barra final en proxy_pass, redirect /api → /api/). Documentar la estrategia real en un runbook (SPECIFICATION_PROD.md) evita repetir la depuración en el siguiente deploy.
IA en el flujo de trabajo
Contexto, memoria, tests y aplicación diaria
Mi integración de la IA comenzó con Spec-Driven Development: definir el qué antes del cómo me llevó a prompts atómicos y contexto acotado. Eso evolucionó hacia Engram para memoria por proyecto, un Cerebro Global sincronizado con Git (`brain-push` / `brain-pull`) y reglas Cursor por repo (`.cursor/rules/*.mdc`). En features críticas el criterio de éxito de la spec incluye tests: el agente lee `docs/TESTING.md`, genera specs siguiendo patrones del monorepo y ejecuta la suite antes de cerrar. Uso la IA como acelerador con reglas, no como sustituto de criterio.
01
Spec-Driven Development
Trabajo con specs acotadas y criterios de aceptación verificables: una intención por mensaje, sin mezclar refactor, diseño y bugfix. La spec incluye objetivo, restricciones, archivos de referencia obligatorios (`.cursor/rules/project.mdc`, `DESIGN.md`) y criterio de éxito verificable (typecheck, lint, tests concretos).
02
Testing con el agente
Automatizo la calidad en el mismo hilo de implementación. El agente investiga el árbol de archivos, aplica patrones de `docs/TESTING.md` (Vitest + Angular Testing Library en web, Jest + Supertest en API), crea helpers reutilizables (`*.testing.ts`) y ejecuta suites acotadas (`ng test --include`, `pnpm --filter api run test`) antes de dar la tarea por cerrada. GitHub Actions replica el gate en cada PR.
03
Memoria de Proyecto
Documento decisiones técnicas, ADRs ligeros y aprendizajes de la bitácora en Engram. Estrategias de testing, convenciones Angular o incidencias de deploy quedan en `~/.engram/engram.db` y se exportan con `engram sync` para que el agente retome contexto en la siguiente sesión.
04
Cerebro Global
Centralizo memorias de agente en un repo Git aparte del código del producto (`cerebro-global`). Scripts Bash en `~/.zshrc` (`brain-push`, `brain-pull`, `brain-status`) sincronizan chunks entre la BD local, el export del proyecto y GitHub. Misma memoria en otra máquina sin copiar notas a mano.
05
Revisión y Control
Mantengo el criterio de producto y la responsabilidad final del código que mergeo. Reviso diff, corro lint/typecheck/tests en local y valido que la solución encaja con la spec. Las reglas del repo marcan qué es obligatorio testear (auth, permisos, dinero, validaciones); el resto no se fuerza por inercia.
El objetivo no es escribir más rápido: es mantener contexto, trazabilidad, tests reproducibles y criterio técnico cuando el asistente cambia en cada sesión. Lo que el agente valida en local, CI lo confirma en cada PR.