Saltar al contenido principal

Práctica 601. Servicio de mensajería.

En esta práctica desarrollarás ChatApp, un servicio de mensajería simplificado inspirado en aplicaciones como WhatsApp, Telegram o Signal. El objetivo no es replicar estas plataformas, sino comprender desde los cimientos cómo funciona una aplicación web en tiempo real: desde la autenticación de usuarios hasta la comunicación bidireccional mediante WebSockets, pasando por una API REST bien estructurada y la integración con un modelo de lenguaje local (LLM).

El proyecto se construye de forma incremental en varias fases. Cada fase añade una capa de funcionalidad. Al finalizar la práctica dispondrás de un sistema funcional completo.

Importante

Al tratarse de un proyecto de mayor tamaño que las anteriores prácticas, en primer lugar, lee todos los apartados y, después, empieza a desarrollar siguiendo los pasos que se van indicando en el apartado Fases del proyecto.

Objetivos

  • Diseñar y desarrollar una API REST con Node.js y Express.
  • Gestionar sesiones y autenticación segura con JWT.
  • Modelar y persistir datos con una base de datos relacional.
  • Implementar comunicación bidireccional en tiempo real mediante WebSockets.
  • Subir y gestionar ficheros en el servidor.
  • Integrar un LLM local a través de la API de Ollama.
  • Aplicar buenas prácticas de estructura de proyectos y variables de entorno.

Arquitectura web

La arquitectura del proyecto es una arquitectura híbrida: monolito backend + SPA frontend. En esta práctica se desarrollará el monolito backend. Para la parte de frontend se proporciona una aplicación completa.

El SPA frontend implementa actualizaciones optimistas para mejorar la experiencia del usuario. Por ejemplo, cuando el usuario envía un mensaje, este aparece en la lista de mensajes de forma inmediata, sin esperar a que el servidor confirme la recepción.

Demo de la aplicación:

  • Frontend: con lo que interactuaría el usuario.
  • Backend: lo que se debe desarrollar.
Importante

La demo está desplegada en Render, plataforma que ofrece servicios gratuitos con limitaciones. Una de ellas es que los servicios se duermen después de un tiempo de inactividad.

Si el frontend no responde, es porque es servicio se ha dormido. Accede al Backend para despertarlo.

Requisitos técnicos

El stack tecnológico obligatorio para el backend es:

  • Entorno (runtime): Node.js.
  • Framework web: Express.js.
  • Base de datos: MariaDB (desplegada a través de Docker Compose).
  • ORM: Prisma ORM. También se puede utilizar Sequelize (no se ha explicado en el módulo).
  • Comunicación en tiempo real: Socket.IO.
  • Autenticación: JSON Web Tokens (JWT) y argon2 para el hasheo de contraseñas.
  • Gestión de archivos: Multer.
  • Inteligencia artificial: Ollama.

Para las pruebas con el backend necesitaremos un cliente REST (Postman o similar) y un navegador web.

Modelo de datos (Prisma)

Se debe definir un esquema de Prisma que soporte las siguientes entidades:

  • User: Representa a un usuario.
    • id
    • username (único)
    • password (guardar hasheada, no en claro)
    • profilePic (opcional)
    • lastSeen (fecha)
    • createdAt
  • Contact: Relación de "contactos" entre usuarios.
  • Chat: Representa una conversación.
    • isGroup (booleano)
    • name (para grupos)
    • profilePic (para grupos)
    • createdAt
  • ChatUser: Tabla de unión entre User y Chat para soportar muchos a muchos. Debe incluir un campo unreadCount para mensajes no leídos por cada usuario en ese chat.
  • Message: Representa un mensaje.
    • id
    • content (texto largo)
    • fileUrl (para archivos adjuntos)
    • fileName
    • createdAt
    • chatId
    • senderId
Borrado en cascada

Se deben configurar correctamente los borrados en cascada (onDelete: Cascade) para que al borrar un chat se eliminen sus mensajes y relaciones de usuario asociados.

Modificación del modelo de datos

Si por algún motivo de diseño, consideras necesario modificar el modelo de datos, puedes hacerlo, pero debes justificar tus cambios en el informe.

Especificación de la API REST

Todas las rutas (excepto login/registro) deben estar protegidas por un middleware de autenticación que verifique el token JWT.

Para ver qué información se debe enviar, qué información se recibe y qué códigos HTTP puede devolver un endpoint, se debe consultar la documentación de la API REST.

Comunicación en tiempo real

El servidor debe gestionar WebSockets (con Socket.IO) y autenticarlos mediante el token JWT.

Eventos que el servidor debe escuchar:

  • connection — Registrar al usuario como "online" y emitir su estado a sus contactos.
  • disconnect — Registrar la fecha de lastSeen y notificar que el usuario está "offline".
  • join_chat — Unirse a salas de socket específicas por chatId.
  • leave_chat — Salir de salas de socket específicas por chatId.
  • typing_start — Retransmitir al resto de miembros del chat que un usuario está escribiendo.
  • typing_end — Retransmitir al resto de miembros del chat que un usuario ha dejado de escribir.

Eventos que el servidor debe emitir:

  • user_status — Notifica cambios de estado (online/offline/lastSeen).
  • new_message — Enviado a todos los miembros de un chat cuando llega un mensaje nuevo.
  • chat_updated — Notifica cambios en el chat (nuevo mensaje, actualización de conteo de no leídos).
  • new_chat — Notifica a un usuario cuando ha sido incluido en un nuevo chat.
  • chat_deleted — Notifica que un chat ha sido eliminado.

Fases del proyecto

El proyecto se divide en cinco fases. Cada fase es incremental (incluye todos los cambios de las anteriores), por lo que el proyecto debería estar siempre en un estado funcional.

Fase 1 — Inicialización del proyecto

En esta fase inicializaremos todo el proyecto con lo básico para poder trabajar en él.

Realiza los siguientes pasos:

  • Configurar el entorno con Docker Compose para levantar MariaDB.
  • Inicializar el proyecto de Node.js con:
    npm init -y
  • Instalar las dependencias con:
    npm install \ 
    express \
    socket.io \
    multer \
    cors \
    jsonwebtoken \
    dotenv \
    @node-rs/argon2 \
    @prisma/client \
    @prisma/adapter-mariadb
  • Instalar las dependencias de desarrollo:
    npm install -D prisma @types/node
  • Configurar conexión a base de datos (MariaDB) con Prisma ORM.
  • Definir el esquema en schema.prisma y ejecutar las migraciones iniciales.
  • Crear el servidor Express básico que devuelva una respuesta en http://localhost:4000/. – En el servidor Express, conectar el Prisma Client.
  • Crear un .env con variables de entorno:
    • JWT_SECRET
    • DB_URL
    • PORT=4000
  • Importar las variables de entorno con dotenv o nativamente.

Crea una estructura de directorios como la siguiente:

chatapp-backend/
├── middleware/ # middlewares: autenticación, subida de ficheros
├── prisma/ # directorio con el modelo Prisma ORM
├── routes/ # rutas: auth.routes.js, messages.routes.js...
├── services/ # servicios: prisma, sockets, ollama
├── uploads/ # ficheros subidos (en .gitignore todos los ficheros que contiene)
├── .env # variables de entorno (en .gitignore)
├── .env.example # plantilla de variables de entorno
├── .gitignore # fichero de Git
├── compose.yml # fichero Docker Compose con MariaDB y Ollama
├── index.js # punto de entrada de la aplicación
├── package.json # dependencias de Node.js
└── README.md # memoria técnica y más instrucciones
Elección de estructura de directorios

La estructura anterior es una recomendación de organización. Puedes utilizar una estructura diferente.

Fase 2 — Autenticación de usuarios

Se programará el registro e inicio de sesión de usuarios con JWT.

Realiza los siguientes pasos:

  • Crear el modelo User en Prisma ORM.
  • Realizar la migración.
  • Crear las rutas para autenticación:
    • POST /api/auth/register — Registro con email y contraseña. Se debe aplicar el hash argon2 a la contraseña recibida. Recibe username y password. Devuelve el token y los datos básicos del usuario.
    • POST /api/auth/login — Verifica las credenciales y devuelve el token JWT.
    • GET /api/auth/me — Devuelve los datos del usuario autenticado (middleware authenticateToken).
  • Crea la ruta para modificar el nombre del usuario:
    • PUT /api/users/username — Cambia el nombre del usuario actual (middleware authenticateToken).
  • Probar con un cliente REST (Postman, Apidog, Insomnia, etc.) que el token se genera y valida correctamente.
  • Crear los eventos de WebSockets que el servidor debe escuchar:
    • connection — Registrar al usuario como "online" y emitir su estado a sus contactos.
    • disconnect — Registrar la fecha de lastSeen y notificar que el usuario está "offline".

Al final de esta fase, el usuario deberá poder registrarse e iniciar sesión.

Fase 3 — Chat individual y grupal

Esta fase consiste en implementar la mensajería directa entre dos usuarios (chat individual) y entre varios usuarios (chat grupal).

Realiza los siguientes pasos:

  • Integrar Socket.IO en el servidor Express.
  • Autenticar el handshake con el JWT (middleware socket).
  • Crear los modelos Contact, Chat y Message en Prisma ORM.
  • Crear la funcionalidad de añadir contactos. Para ello, añade las siguientes rutas:
    • GET /api/users — Lista de usuarios registrados (excluye al propio usuario).
    • GET /api/users/search?q=... — Busca usuarios por nombre.
    • POST /api/contacts — Añade un usuario a la lista de contactos (recibe contactId).
    • GET /api/contacts — Lista los contactos añadidos por el usuario.
  • Crear la funcionalidad para crear/eliminar chats. Para ello, añade las siguientes rutas:
    • POST /api/chats — Crea un chat. Si isGroup es false y ya existe un chat 1-a-1 con ese usuario, devuelve el existente.
    • GET /api/chats — Lista todos los chats del usuario actual, incluyendo el último mensaje y el conteo de no leídos.
    • DELETE /api/chats/:chatId — Elimina un chat completo (solo si el usuario pertenece a él).
  • Crear la funcionalidad para enviar mensajes. Para ello, añade las siguientes rutas:
    • POST /api/messages — Envía un mensaje. De momento, sólo soporta texto.
    • GET /api/chats/:chatId/messages — Obtiene el historial de mensajes de un chat.
    • POST /api/chats/:chatId/read — Marca todos los mensajes de un chat como leídos para el usuario actual (pone unreadCount a 0).
  • Validar el correcto funcionamiento de los endpoisnt con un cliente REST.

Fase 4 — Chat en tiempo real

A continuación, implementaremos las comunicaciones en tiempo real para los chats.

Realiza los siguientes pasos:

  • Crear los eventos de WebSockets que el servidor debe escuchar:
    • join_chat — Un usuario se une a un chat.
    • leave_chat — Un usuario sale de un chat.
    • typing_start — Retransmitir al resto de miembros del chat que un usuario está escribiendo.
    • typing_end — Retransmitir al resto de miembros del chat que un usuario ha dejado de escribir.
  • Crear los eventos de WebSockets que el servidor debe emitir:
    • new_message — Enviado a todos los miembros de un chat cuando llega un mensaje nuevo.
    • chat_updated — Notifica cambios en el chat (nuevo mensaje, actualización de conteo de no leídos).
    • new_chat — Notifica a un usuario cuando ha sido incluido en un nuevo chat.
    • chat_deleted — Notifica que un chat ha sido eliminado.

Al final de esta fase, el usuario deberá poder añadir contactos y establecer conversación con los contactos añadidos.

Fase 5 — Envío de ficheros

Esta fase consistirá en permitir compartir ficheros en chats y subir imágenes de perfil de usuario.

Realiza los siguientes pasos:

  • Integrar Multer para permitir subir fotos de perfil y archivos en los mensajes.
  • Todos los ficheros se guardarán en el directorio uploads.
  • El límite de subida debe ser de 10 MB.
  • Modificar la funcionalidad para enviar mensajes. Para ello, modifica las siguientes rutas:
    • POST /api/messages — Envía un mensaje. Ahora, añade la posibilidad de enviar ficheros.
  • Servir los ficheros de forma estática sin autenticación (utiliza static de Express).
  • Crear la funcionalidad de actualizar imagen de perfil. Para ello, añade la siguiente ruta:
    • POST /api/users/profile-pic — Sube una imagen de perfil (usar Multer).

Fase 6 — Chat con LLM (Ollama)

Por último, esta fase consistirá en implementar una integración con Ollama. El usuario podrá escribir a un asistente de IA local como si fuese cualquier otro usuario.

Realiza los siguientes pasos:

  • Configurar el entorno con Docker Compose para levantar Ollama.
  • Accede al contenedor de Ollama y descarga un modelo ligero.
  • Si el usuario abre un chat con un usuario especial llamado bot, todos los mensajes que el usuario envía, serán reenviados a la API de Ollama. La respuesta devuelta por el modelo será mostrada al usuario. Se mostrará el mensaje cuando el modelo haya completado la generación de la respuesta (no se utilizarán streams).
  • El usuario bot debe crearse cuando se arranca el servidor. Si el usuario bot ya existe, no se debe realizar ninguna acción.

Requisitos de desarrollo

Todo el proyecto se debe desarrollar en un repositorio Git.

Importante

El proyecto debe:

  • Estar en un repositorio privado en GitHub.
  • Estar compartido con la cuenta inf-fp.

El repositorio debe contener:

  • Mínimo, un commit por sesión de trabajo. Los mensajes de commit deben ser descriptivos.
  • El fichero .env nunca debe incluirse en el repositorio. En su lugar, proporcionar un fichero .env.example a modo plantilla con todos los campos necesarios, pero sin valores reales.
  • Los ficheros del directorio uploads/ nunca deben incluirse en el repositorio. Por lo tanto, el directorio uploads/ debe estar en .gitignore.
  • El README.md debe incluir una memoria técnica que explique para cada fase:
    • Decisiones de diseño.
    • Dificultades encontradas.
    • Soluciones encontradas.
  • El REAMDE.md puede incluir también otra información adicional.

Entrega final

El día de la entrega final, se deben cumplir los siguientes puntos:

  • Repositorio con todas las fases integradas en la rama principal (main).
  • La aplicación debe arrancar con npm install && npm start sin errores.

Sólo se evaluarán los commits realizados hasta la fecha de entrega final.

Evaluación

Se evaluará el proyecto por las fases que se han desarrollado de forma completa. La puntuación total del proyecto será la suma de las puntuaciones de todas las fases completadas. La nota máxima que se podrá obtener será un 10.

FaseDescripciónPuntuación
1Inicialización del proyecto0.5 puntos
2Autenticación de usuarios1.5 puntos
3Chat individual y grupal4 puntos
4Chat en tiempo real2 puntos
5Envío de ficheros1 puntos
6Chat con LLM (Ollama)1 puntos

El día del examen se realizará una entrevista oral sobre el desarrollo de la aplicación.