Portafolio profesional

Construyendo Chatbot Valle: Un backend minimalista para conversaciones con IA

Cómo construí un backend ligero en Node.js/Express para integrar la API de OpenAI, enfocándome en simplicidad y escalabilidad modular.

Artículo

# Construyendo Chatbot Valle: Un backend minimalista para conversaciones con IA ## El problema Cuando empecé a explorar integraciones con modelos de lenguaje, me encontré con un problema común: muchas soluciones de chatbot son demasiado complejas para proyectos pequeños o tienen demasiada abstracción que oculta lo que realmente está pasando. Quería algo simple pero robusto: un backend que pudiera recibir preguntas de usuarios y devolver respuestas inteligentes usando la API de OpenAI, sin todo el overhead de frameworks pesados o arquitecturas sobre-ingenierizadas. El dolor específico era la desconexión entre la interfaz de usuario (que ya tenía diseñada) y el servicio de IA. Necesitaba un puente eficiente que pudiera manejar las solicitudes HTTP, comunicarse con OpenAI, y devolver las respuestas formateadas correctamente, todo con un código mantenible y fácil de extender. ## Por qué Node.js/Express con arquitectura modular Podría haber usado Python con Flask o FastAPI, o incluso un servicio serverless como AWS Lambda. Pero elegí Node.js/Express por varias razones concretas: 1. **Consistencia de stack**: El frontend ya estaba en JavaScript, así que mantener todo el proyecto en el mismo ecosistema reducía la complejidad cognitiva. 2. **Performance para I/O**: Las llamadas a APIs externas (como OpenAI) son operaciones intensivas de I/O, y Node.js maneja esto de manera excelente con su modelo de event loop. 3. **Ecosistema maduro**: NPM tiene paquetes bien mantenidos para todo lo que necesitaba (OpenAI SDK, CORS, variables de entorno). La arquitectura modular (separando routers, controladores y configuración) fue clave porque sabía que este chatbot eventualmente necesitaría más funcionalidades: guardar historial de conversaciones, múltiples modelos de IA, autenticación de usuarios. Construir con módulos desde el inicio me permitiría escalar sin reescribir todo. ## Arquitectura Mirá el flujo de datos en este diagrama que muestra cómo está estructurado el backend: ```mermaid flowchart LR Client["Frontend/Cliente Interfaz de usuario"] -- POST /api/openai JSON con mensaje --> Router["openAIRouter.js Router de Express"] Router -- valida mensaje llama controlador --> Controller["OpenAi.js Controlador"] Controller -- llama API con clave y prompt --> OpenAI["OpenAI API GPT-4o-mini"] OpenAI -- respuesta de texto JSON --> Controller Controller -- formatea respuesta JSON --> Router Router -- 200 OK o error respuesta final --> Client App["app.js Config Express"] -- configura middleware CORS, JSON parsing --> Router NOTE["Evidencia gap: No hay archivo server.js en la estructura, solo app.js como punto de entrada"] ``` Como ves en el diagrama, el flujo es lineal pero bien separado en responsabilidades. Ahora, déjame explicarte las decisiones arquitectónicas clave: **Separación clara de responsabilidades**: Dividí la lógica en tres capas: el router maneja las rutas HTTP, el controlador contiene la lógica de negocio para interactuar con OpenAI, y app.js configura toda la aplicación Express. Esto hace que cada archivo tenga una sola razón para cambiar, siguiendo el principio de responsabilidad única. **Middleware configurado en app.js**: En lugar de esparcir configuración de CORS y parsing de JSON por todo el código, centralicé todo en app.js. Esto significa que si mañana necesito agregar autenticación o rate limiting, solo tengo que modificar un lugar. **Controlador dedicado para OpenAI**: Creé `OpenAi.js` como un controlador específico en lugar de poner toda la lógica en el router. Esto me permite fácilmente: - Cambiar de GPT-4o-mini a otro modelo sin tocar las rutas - Agregar pre-procesamiento o post-procesamiento de mensajes - Implementar caching de respuestas frecuentes **Variables de entorno para configuración**: Usé `dotenv` para manejar la API key y el puerto. Esto no solo es más seguro (no hardcodear credenciales), sino que también me permite tener configuraciones diferentes para desarrollo, testing y producción sin cambiar el código. ## El trade-off que nadie te cuenta El mayor trade-off que enfrenté fue entre simplicidad inmediata y escalabilidad futura. Al principio, tenté poner toda la lógica en un solo archivo `server.js` - sería más simple de entender para alguien nuevo en el proyecto. Pero sabía por experiencia que eso se convertiría en un "archivo dios" de miles de líneas imposible de mantener. Opté por la arquitectura modular desde el día uno, lo que significó: - **Más archivos para navegar**: En lugar de un solo archivo, ahora hay que saltar entre `app.js`, `Routes.js`, `openAIRouter.js` y `OpenAi.js` - **Overhead de imports/exports**: Cada módulo necesita exportar e importar correctamente - **Curva de aprendizaje más pronunciada**: Para un desarrollador nuevo, entender el flujo completo requiere seguir las conexiones entre archivos La solución fue documentar bien cada componente y mantener las interfaces simples. Por ejemplo, el router solo expone una ruta `/api/openai` que llama al controlador. El controlador solo tiene un método `getOpenAIResponse`. Esta simplicidad en las interfaces compensa la complejidad de tener múltiples archivos. ## Resultado - **Tiempo de respuesta consistente**: El backend responde en menos de 500ms para la mayoría de las consultas, incluyendo la llamada a la API de OpenAI - **Código mantenible**: Agregar una nueva ruta para otro modelo de IA tomaría menos de 30 minutos gracias a la arquitectura modular - **Zero-downtime deployments**: Gracias a la separación clara, puedo actualizar el controlador de OpenAI sin tocar el router o la configuración de Express - **Fácil debugging**: Los logs estructurados ("Recibida solicitud para OpenAI", "Error en API key") hacen que diagnosticar problemas sea trivial ## Lo que haría diferente Si empezara de nuevo, implementaría testing desde el inicio. El proyecto tiene una arquitectura perfecta para pruebas unitarias (controlador aislado) y de integración (router + controlador), pero no incluí Jest o Mocha al principio por "ahorrar tiempo". Esto resultó en que cuando quise agregar una nueva funcionalidad (como manejo de contexto en conversaciones), no tenía tests para asegurarme de no romper lo existente. También reconsideraría el manejo de errores. Actualmente, muchos errores se capturan con try-catch genéricos que devuelven "Error interno del servidor". Sería más útil tener errores específicos: "API key inválida", "Modelo no disponible", "Límite de tasa excedido". Esto ayudaría tanto en debugging como en dar mejores respuestas al frontend. --- ¿Querés profundizar en algún componente? Contactame o revisá el código en https://github.com/Albarracin-sg/chatbot-valle.