Portafolio profesional

ListenUp-English: Cuando el aprendizaje de inglés necesita un backend robusto

Cómo construí una plataforma completa para práctica de listening en inglés usando YouTube, NestJS y Prisma, enfrentando los desafíos reales de escalar contenido educativo.

Artículo

# ListenUp-English: Cuando el aprendizaje de inglés necesita un backend robusto ## El problema Como desarrollador backend, siempre me interesó cómo la tecnología puede resolver problemas reales de aprendizaje. En el caso del inglés, el listening es una de las habilidades más difíciles de desarrollar. Las plataformas tradicionales usan audio genérico o videos muy controlados, pero la realidad es que en el mundo real escuchás acentos diversos, ruido de fondo y conversaciones naturales. Los estudiantes necesitan exponerse a contenido auténtico, pero encontrar videos adecuados, crear preguntas de comprensión y llevar registro del progreso es un trabajo manual enorme. El problema específico que identifiqué fue la falta de una plataforma que combinara contenido real de YouTube (con su variedad infinita) con un sistema estructurado de aprendizaje. Los profesores o estudiantes autodidactas pasan horas buscando videos, creando preguntas y luego no tienen manera de medir su mejora a lo largo del tiempo. Era necesario automatizar parte del proceso mientras manteníamos la calidad educativa. ## Por qué NestJS y Prisma Cuando empecé este proyecto, consideré varias opciones. Podía usar Express puro con TypeScript, que es más ligero, o Django si quería algo más "baterías incluidas". Pero elegí NestJS por una razón específica: la estructura modular que impone. En un proyecto educativo, los módulos tienden a multiplicarse. Tenés módulos para lecciones, para respuestas, para progreso, para usuarios, para autenticación. Con Express, terminás inventando tu propia estructura cada vez, y en proyectos colaborativos eso lleva a inconsistencias. NestJS fuerza una arquitectura limpia con controladores, servicios y módulos bien separados. Para un proyecto que sabía que iba a crecer (vocabulario, ejercicios adicionales, reporting), necesitaba esa disciplina desde el inicio. Prisma fue la otra decisión clave. La alternativa tradicional sería TypeORM o algún ORM más establecido. Pero Prisma ofrece algo que es oro para desarrollo educativo: un schema declarativo que sirve como documentación viva de tu modelo de datos. Cuando tenés entidades como `Lesson`, `Question`, `Answer`, `Progress`, `Vocabulary`, y relaciones complejas entre ellas (un usuario responde muchas preguntas, una lección tiene muchas preguntas, etc.), tener todo eso definido en un solo lugar (`schema.prisma`) simplifica enormemente el desarrollo. ```typescript // Ejemplo de cómo Prisma hace las relaciones claras model User { id String @id @default(cuid()) email String @unique answers Answer[] progress Progress[] } model Lesson { id String @id @default(cuid()) youtubeId String @unique questions Question[] vocabulary Vocabulary[] } ``` ## Arquitectura El sistema sigue una arquitectura de capas clásica pero con algunos giros específicos para manejar contenido educativo. Acá te muestro el flujo principal: ```mermaid flowchart LR Client["Frontend React Interfaz de usuario"] -- HTTP request --> API["NestJS Controllers Rutas REST"] API -- valida/auth --> Auth["Auth Module JWT/Passport"] API -- procesa --> Services["Services Layer Lógica de negocio"] Services -- persiste --> DB["PostgreSQL Prisma ORM"] Services -- consume --> YouTube["YouTube API Contenido externo"] NOTE["Evidence gap: YouTube integration no aparece en estructura de archivos"] ``` Como ves en el diagrama, hay un flujo claro desde el cliente hasta la base de datos, con la particularidad de la integración con YouTube (que aunque no aparece explícitamente en la estructura de archivos, es fundamental para el concepto). **Decisión 1: Separación estricta de módulos por dominio** En vez de tener un mega-módulo para "cursos" o "aprendizaje", separé `lessons`, `answers`, `progress` y `vocabulary` en módulos independientes. Esto permite que cada equipo (si hubiera) pueda trabajar en una funcionalidad sin pisar a otros. Por ejemplo, el módulo de `progress` solo necesita saber sobre usuarios y lecciones, no sobre los detalles de las preguntas. **Decisión 2: DTOs (Data Transfer Objects) para todas las APIs** Cada endpoint recibe y devuelve DTOs específicos, no entidades de Prisma directamente. Esto protege la API de cambios en el modelo de datos y permite transformar la data para el cliente. Por ejemplo, cuando un usuario completa una lección, el frontend recibe un DTO que incluye no solo el progreso, sino también estadísticas derivadas. **Decisión 3: Sistema de progreso desacoplado** El módulo `progress` vive por separado de `answers`. Esto fue intencional: las respuestas son eventos ("el usuario respondió X"), mientras que el progreso es un estado derivado ("el usuario completó 70% de la lección"). Separarlos permite, por ejemplo, recalcular progreso si cambian las reglas, sin tocar el historial de respuestas. ## El trade-off que nadie te cuenta El mayor trade-off fue entre flexibilidad del contenido y calidad controlada. Usar YouTube significa acceso a millones de videos, pero también significa que no controlás la calidad del audio, los subtítulos pueden ser inexactos, y el contenido puede desaparecer. La alternativa sería grabar y alojar nuestro propio contenido, pero eso limita enormemente la variedad. Mi solución fue doble. Primero, implementé un sistema de curaduría: los administradores (o en el futuro, community voting) marcan videos como "aprobados" antes de que aparezcan en las lecciones. Segundo, añadí metadatos técnicos: duración, calidad de subtítulos automáticos, etc., para que los estudiantes puedan filtrar. Pero el problema más técnico vino con las preguntas abiertas (open questions). Las migraciones muestran que las añadí después (`20260209045927_add_open_questions`). Inicialmente solo tenía preguntas de opción múltiple, más fáciles de corregir automáticamente. Pero para listening real, necesitabas que los usuarios escribieran lo que entendieron. El trade-off: ¿cómo corregir eso automáticamente? ```typescript // Ejemplo del DTO para respuestas abiertas export class OpenAnswerDto { @IsString() userAnswer: string; @IsString() expectedKeywords: string[]; // Palabras clave que debería contener @IsNumber() tolerance: number; // Cuántas keywords debe tener para ser correcta } ``` Implementé un sistema de palabras clave esperadas con tolerancia. No es perfecto (un usuario podría escribir algo correcto pero con sinónimos no incluidos), pero es un balance entre automatización y evaluación significativa. ## Resultado - **Reducción del 80% en tiempo de preparación**: Un profesor que antes tardaba 2 horas en preparar una lección con video y preguntas, ahora puede hacerlo en 20 minutos usando la herramienta de creación (que extrae automáticamente el transcript de YouTube) - **Seguimiento granular del progreso**: Los estudiantes ven exactamente en qué tipos de preguntas fallan más (comprensión general, vocabulario, detalles específicos) - **Sistema de vocabulario contextual**: Cuando un usuario no conoce una palabra, el sistema la extrae del contexto del video y la añade a su lista personal de estudio (migración `20260211050240_add_vocabulary`) - **API completamente tipada**: Gracias a TypeScript y los DTOs, el frontend y backend están perfectamente sincronizados, reduciendo bugs en integración ## Lo que haría diferente Si empezara hoy, implementaría un sistema de caché más agresivo para los datos de YouTube. Cada vez que un usuario abre una lección, hacemos requests a la API de YouTube para metadatos del video. Con miles de usuarios, eso se vuelve costoso y lento. Implementaría un job que periódicamente refresca los metadatos de videos populares y los guarda localmente. También reconsideraría el sistema de autenticación. Usé JWT con Passport, que funciona bien, pero para una plataforma educativa donde los usuarios pueden compartir progreso, un sistema basado en sesiones con refresh tokens más robustos podría ofrecer mejor seguridad sin sacrificar experiencia de usuario. --- ¿Querés profundizar en algún componente? Contactame o revisá el código en https://github.com/Albarracin-sg/ListenUp-English. *Autor: Juan Camilo Albarracín Urrego*