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.
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:
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.idusername(ú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 entreUseryChatpara soportar muchos a muchos. Debe incluir un campounreadCountpara mensajes no leídos por cada usuario en ese chat.Message: Representa un mensaje.idcontent(texto largo)fileUrl(para archivos adjuntos)fileNamecreatedAtchatIdsenderId
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.
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 delastSeeny notificar que el usuario está "offline".join_chat— Unirse a salas de socket específicas porchatId.leave_chat— Salir de salas de socket específicas porchatId.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.prismay 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
.envcon variables de entorno:JWT_SECRETDB_URLPORT=4000
- Importar las variables de entorno con
dotenvo 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
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
Useren 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. Recibeusernameypassword. 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 (middlewareauthenticateToken).
- Crea la ruta para modificar el nombre del usuario:
PUT /api/users/username— Cambia el nombre del usuario actual (middlewareauthenticateToken).
- 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 delastSeeny 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,ChatyMessageen 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 (recibecontactId).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. SiisGroupesfalsey 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 (poneunreadCounta 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
staticde 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
botdebe crearse cuando se arranca el servidor. Si el usuariobotya existe, no se debe realizar ninguna acción.
Requisitos de desarrollo
Todo el proyecto se debe desarrollar en un repositorio Git.
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
.envnunca debe incluirse en el repositorio. En su lugar, proporcionar un fichero.env.examplea 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 directoriouploads/debe estar en.gitignore. - El
README.mddebe incluir una memoria técnica que explique para cada fase:- Decisiones de diseño.
- Dificultades encontradas.
- Soluciones encontradas.
- El
REAMDE.mdpuede 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 startsin 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.
| Fase | Descripción | Puntuación |
|---|---|---|
| 1 | Inicialización del proyecto | 0.5 puntos |
| 2 | Autenticación de usuarios | 1.5 puntos |
| 3 | Chat individual y grupal | 4 puntos |
| 4 | Chat en tiempo real | 2 puntos |
| 5 | Envío de ficheros | 1 puntos |
| 6 | Chat con LLM (Ollama) | 1 puntos |
El día del examen se realizará una entrevista oral sobre el desarrollo de la aplicación.