All projects
In progress

Software Engineering | School | 8 min read

Real-time Messaging API.

Messaging in real time, done properly: REST API, WebSocket, JWT authentication and JSON so messages arrive instantly and only to the right people.


ChatAPI

capture_principale
capture_principale

Messagerie instantanée entre deux appareils distincts, réalisée pour le projet de synthèse du module API & Web Services (cycle ingénieur, EFREI).

Deux utilisateurs, l'un sur un téléphone et l'autre sur un ordinateur, échangent des messages en temps réel. Les appareils ne communiquent jamais directement : chaque message passe par le serveur ChatAPI, qui l'enregistre puis le retransmet au destinataire.

demo_bureau
demo_bureau

Fonctionnalités

FonctionnalitéTechnologieÉtat
Création de compte et connexionREST + JWTFait
Liste des utilisateurs avec leur statut en ligneRESTFait
Historique d'une conversationRESTFait
Protection des endpoints (401 sans jeton valide)Spring Security + JWTFait
Envoi et réception en direct, message enregistré avant livraisonWebSocketFait
Présence en ligne et hors ligne en directWebSocketFait
Deux clients distincts : mobile.html (téléphone) et desktop.html (ordinateur)HTML / CSS / JavaScriptFait
Bonus : accusés de réception et de lecture (envoyé, livré, lu)WebSocketFait
Bonus : indicateur de saisieWebSocketFait
Bonus : notification hors ligne par webhookHTTP sortantFait
Bonus : limitation de débit des messagesWebSocketFait
Bonus : documentation OpenAPI et Swagger UIRESTFait

Lancer le projet

Prérequis :

  • Java 17 ou plus récent : Gradle et le JDK 21 du projet sont téléchargés automatiquement ;
  • PostgreSQL démarré, avec une base chatapiDB créée au préalable. Hibernate crée les tables au premier lancement, mais pas la base elle-même :
psql -U postgres -c "CREATE DATABASE \"chatapiDB\";"

Le mot de passe PostgreSQL est lu dans la variable DB_PASSWORD. Sans elle, le serveur ne démarre pas.

cd chatapi
$env:DB_PASSWORD='mot_de_passe'   # Windows
.\gradlew.bat bootRun
cd chatapi
DB_PASSWORD='mot_de_passe' ./gradlew bootRun   # macOS / Linux

Le serveur démarre sur http://localhost:8080. Le premier lancement est plus long, le temps de télécharger les dépendances.

Ouvrir http://localhost:8080 dans le navigateur redirige vers le client adapté à l'appareil :

ClientURLDisposition
Mobilehttp://<hôte>:8080/mobile.htmlUne vue à la fois : liste des contacts, puis conversation en plein écran
Bureauhttp://<hôte>:8080/desktop.htmlContacts et conversation côte à côte, touche Échap pour fermer la conversation

Les deux clients partagent la même logique (chat.js) et le même thème (chat.css) ; seule la mise en page diffère. Pour essayer seul sur un ordinateur, ouvrir deux onglets et se connecter avec un compte différent dans chacun (par exemple seyni et fode) : chaque onglet garde son propre jeton.

Par défaut :

  • les données sont enregistrées dans la base PostgreSQL chatapiDB, elles sont conservées entre deux redémarrages ;
  • sans variable JWT_SECRET, une clé de signature aléatoire est générée au démarrage. Les jetons sont alors invalidés à chaque redémarrage : il suffit de se reconnecter.

Lancer les tests

cd chatapi
.\gradlew.bat test

Les tests utilisent une base H2 en mémoire et ne touchent pas aux données.

Configuration

VariableRôleDéfaut
JWT_SECRETClé de signature des jetons (32 caractères minimum)Clé aléatoire à chaque démarrage
CHAT_WEBHOOK_URLURL appelée en POST quand le destinataire d'un message est hors ligneAucune : notification simulée dans les logs
DB_URLURL PostgreSQLjdbc:postgresql://localhost:5432/chatapiDB
DB_USERUtilisateur PostgreSQLpostgres
DB_PASSWORDMot de passe PostgreSQL (obligatoire)aucun

API REST

Tous les échanges sont en JSON. Sauf la création de compte et la connexion, chaque requête doit porter l'en-tête Authorization: Bearer <jeton>, sinon le serveur répond 401 Unauthorized.

MéthodeURLRôleRéponses
POST/api/comptesCréer un compte201, 400 données invalides, 409 nom déjà pris
POST/api/auth/loginSe connecter et recevoir un JWT200, 401 identifiants invalides
GET/api/utilisateursLister les utilisateurs et leur statut enLigne200, 401
GET/api/messages/{utilisateurId}Historique avec un utilisateur200, 401

Créer un compte puis se connecter

POST /api/comptes
Content-Type: application/json

{ "username": "seyni", "password": "secret123" }
POST /api/auth/login
Content-Type: application/json

{ "username": "seyni", "password": "secret123" }
{ "token": "eyJhbGciOiJIUzI1NiJ9...", "type": "Bearer", "id": 1, "username": "seyni" }

Message de l'historique

{ "id": 1, "expediteurId": 1, "destinataireId": 2, "contenu": "Bonjour !", "dateEnvoi": "2026-09-21T10:15:30", "statut": "LU" }

La documentation OpenAPI est consultable sur http://localhost:8080/swagger-ui.html : après la connexion, coller le jeton dans "Authorize" pour essayer les endpoints protégés. Une collection Postman documentée et testée est aussi fournie dans postman/.

WebSocket

Le temps réel passe par une connexion WebSocket ouverte avec le jeton obtenu à la connexion :

ws://localhost:8080/ws/messages?token=<jeton>

Sans jeton valide, la connexion est refusée (401). Le jeton est passé dans l'URL car un navigateur ne peut pas ajouter d'en-tête Authorization à une connexion WebSocket.

TypeSensContenu
messageclient vers serveurdestinataireId, contenu
messageserveur vers destinataire et expéditeurid, expediteurId, destinataireId, contenu, dateEnvoi, statut
accuseserveur vers expéditeurmessageId, statut (LIVRE ou LU)
luclient vers serveurinterlocuteurId : la conversation est affichée
saisieclient vers serveurdestinataireId, enCours
saisieserveur vers destinataireexpediteurId, enCours
presenceserveur vers les autres utilisateursutilisateurId, enLigne
erreurserveur vers l'émetteurcode (FORMAT_INVALIDE, TYPE_INCONNU, MESSAGE_INVALIDE, LIMITE_DEPASSEE), detail

Envoyer un message

{ "type": "message", "destinataireId": 2, "contenu": "Bonjour !" }

Message reçu par le destinataire

{ "type": "message", "id": 1, "expediteurId": 1, "destinataireId": 2, "contenu": "Bonjour !", "dateEnvoi": "2026-09-21T10:15:30", "statut": "ENVOYE" }

L'expéditeur est toujours celui du jeton et la date celle du serveur : un expediteurId ou une dateEnvoi envoyés par le client sont ignorés.

Cycle d'un message

  1. Le message est enregistré en base avec le statut ENVOYE, avant toute tentative de livraison.
  2. Il est renvoyé à l'expéditeur, qui obtient ainsi son id et la date du serveur.
  3. Si le destinataire est connecté, il le reçoit immédiatement, le statut passe à LIVRE et l'expéditeur reçoit un accuse.
  4. Sinon, le message attend en base et un webhook prévient un service tiers (simulé dans les logs si CHAT_WEBHOOK_URL n'est pas défini). À la connexion du destinataire, le statut passe à LIVRE.
  5. Quand le destinataire ouvre la conversation, il envoie lu : les messages passent à LU et l'expéditeur reçoit un accuse.

Un statut n'avance jamais en arrière. Chaque utilisateur peut envoyer au plus 20 messages sur toute période de 10 secondes ; au-delà, il reçoit une erreur LIMITE_DEPASSEE.

Tester depuis un téléphone

  1. Le téléphone et le PC sont sur le même réseau Wi-Fi.
  2. Récupérer l'adresse IPv4 du PC avec ipconfig.
  3. Autoriser le port 8080 dans le pare-feu Windows (réseau privé).
  4. Ouvrir http://<IPv4 du PC>:8080 sur le téléphone.

Un VPN actif sur le PC peut bloquer l'accès depuis le réseau local : autoriser le réseau local dans ses réglages ou le désactiver le temps du test.

Technologies

Java 21, Spring Boot 4.1 (Web MVC, WebSocket, Security, Data JPA, Validation), Gradle 9, JWT (jjwt), springdoc-openapi (Swagger UI), PostgreSQL (H2 en mémoire pour les tests), Lombok, HTML / CSS / JavaScript.

Structure du projet

API-et-Web-Services/
├── README.md
├── sujet.pdf                                sujet du projet
├── postman/                                 collection Postman à importer
└── chatapi/                                 serveur Spring Boot
    ├── build.gradle, settings.gradle
    ├── gradlew, gradlew.bat                 Gradle fourni, rien à installer
    └── src/
        ├── main/java/fr/efrei/chatapi/
        │   ├── controller/                  endpoints REST
        │   │   ├── CompteController         POST /api/comptes
        │   │   ├── AuthController           POST /api/auth/login
        │   │   ├── UtilisateurController    GET  /api/utilisateurs
        │   │   └── MessageController        GET  /api/messages/{id}
        │   ├── websocket/                   temps réel
        │   │   ├── WebSocketConfig          point d'entrée /ws/messages
        │   │   ├── JwtHandshakeInterceptor  vérification du jeton à l'ouverture
        │   │   ├── SessionRegistry          connexions ouvertes par utilisateur
        │   │   └── MessageWebSocketHandler  réception, enregistrement et relais
        │   ├── security/                    SecurityConfig, JwtService, JwtAuthFilter
        │   ├── service/                     logique métier
        │   │   ├── UtilisateurService, MessageService
        │   │   ├── NotificationService      webhook hors ligne
        │   │   └── RateLimiter              limitation de débit
        │   ├── config/                      OpenApiConfig (documentation Swagger)
        │   ├── repository/                  accès à la base (Spring Data JPA)
        │   ├── model/                       entités Utilisateur et Message, StatutMessage
        │   └── dto/                         objets JSON échangés (records)
        ├── main/resources/
        │   ├── static/                          clients web : mobile.html, desktop.html,
        │   │                                    chat.js et chat.css partagés, mobile.css, desktop.css
        │   ├── application.properties           configuration générale, active le profil postgres
        │   └── application-postgres.properties  connexion PostgreSQL (variables DB_*)
        └── test/                            tests JUnit : API REST (MockMvc), WebSocket, limitation de débit

Projet réalisé par Fode Laye Keita et Balde Seyni Junior, en deuxième année du cycle ingénieur Big Data & Machine Learning, EFREI.