1. Contexte : client et serveur
Si l'on regarde le projet d'en haut, le niveau actuel est le moment où ReadLater Starter cesse d'être une « application console avec un client HTTP » et commence à se comporter comme un petit backend. Ce n'est pas « juste un mode pour cocher une case », mais un changement de rôle : auparavant, nous étions celui qui frappe à l'API d'autrui, maintenant nous sommes celui que Postman, un navigateur ou un autre client viendront solliciter. Et c'est ici que se produit une chose magique (en fait, technique) : l'application ne « fait plus son travail puis s'en va », elle « monte la garde » et attend les requêtes entrantes.
Un simple tableau comparatif aide bien. Il ne s'agit pas de « qui est le meilleur », mais du changement de contexte dans l'esprit du développeur :
| Critère | Client-mode (catalog ...) | Server-mode (server) |
|---|---|---|
| Qui initie l'action | notre application | client externe (Postman/navigateur/curl) |
| Direction HTTP | requêtes sortantes | requêtes entrantes |
| Cycle de vie | a fait → a affiché → s'est terminé | s'est lancé → écoute le port → vit longtemps |
| Critère principal « fonctionne » | nous avons reçu la réponse attendue du fournisseur | le client a reçu une réponse correcte de notre part |
| Outil de vérification | logs + sortie console | Postman/navigateur + logs |
C'est là le changement clé : nous apprenons à ne pas penser « j'ai appelé une méthode et j'ai obtenu un résultat », mais « quelqu'un de l'extérieur a fait une requête, et je dois renvoyer une réponse HTTP correcte ».
Mais l'idée même du server-mode est utile jusqu'au moment où l'on commence à se demander : qui écoute le port, qui associe le chemin, et via quel objet la réponse est-elle formée ? Dès que ces rôles deviennent clairs, le serveur cesse de ressembler à de la magie et devient un simple mécanisme d'ingénierie.
2. Le mode server dans l'application
Au début, on a très envie de faire vite : « bon, ajoutons un if dans main() et démarrons le serveur ». C'est un peu comme réparer une voiture avec du ruban adhésif : parfois ça marche, mais après on a honte même devant soi-même (et parfois il y a aussi de la fumée). Nous introduisons un mode de lancement explicite SERVER, parce que le projet doit rester un seul artefact avec un comportement prévisible. Nous avons déjà convenu que l'application a des modes CATALOG_SEARCH, CATALOG_DETAILS, et maintenant un troisième apparaît — SERVER. Cela rend le lancement lisible, le logging clair et la configuration applicable sans complications.
Plus important encore : en server-mode, l'application a un cycle de vie différent. Si le client-mode ressemble à « aller au magasin chercher du pain » (aller, acheter, revenir), le server-mode ressemble à « ouvrir un magasin » (mettre un panneau, allumer la caisse et rester toute la journée). Et si ces deux scénarios sont mélangés dans une seule branche implicite, vous obtiendrez rapidement une application difficile à lancer, difficile à expliquer et encore plus difficile à maintenir.
La « fixation » minimale du mode ressemble à ceci — sans chaînes magiques, sans if cachés :
public enum LaunchMode {
// Mode client : effectue une requête sortante pour la recherche
CATALOG_SEARCH,
// Mode client : effectue une requête sortante pour les détails
CATALOG_DETAILS,
// Mode serveur : démarre le serveur HTTP et attend les requêtes entrantes
SERVER
}
Et dans le point d'entrée, nous choisissons explicitement la branche de comportement. Même s'il y a plus tard une multitude de détails à l'intérieur, la bifurcation elle-même doit être simple et lisible :
import static com.example.readlater.app.LaunchMode.*;
// launchMode est calculé à partir des arguments/config avant ce point
switch (launchMode) {
case CATALOG_SEARCH -> runCatalogSearch(args);
case CATALOG_DETAILS -> runCatalogDetails(args);
// Important : ici commence le mode « longue durée » de l'application
case SERVER -> startServer(appConfig);
}
Remarquez à quel point c'est « la simplicité même » : nous n'essayons pas d'être plus intelligents que nécessaire. Pour l'instant, il est plus important que l'étudiant puisse lire le comportement de l'application et comprendre : « ah, voici le mode serveur, voilà les modes client ».
3. Objectif du server-mode aujourd'hui
Quand on entend « mode serveur », le cerveau dessine immédiatement quelque chose de grand : « endpoints, bases de données, authentification, belles réponses JSON, et où est Swagger ? ». Calme. Nous gardons délibérément un focus étroit : aujourd'hui, il est important de voir le simple fait d'une requête HTTP entrante et le simple fait d'une réponse correcte. Tout le reste est la couche de complexité suivante, et si on y saute directement, la phase serveur se transformera en un fouillis de détails.
Dans un projet backend normal, il y a un endpoint technique, ennuyeux mais incroyablement utile — le health check. Il répond à une question simple : « le service est-il vivant ? ». Pas « toutes les fonctionnalités fonctionnent », pas « la logique métier est parfaite », mais simplement « le processus est démarré et capable de répondre sur le réseau ». Nous commencerons par cela, car c'est le contrat minimal le plus honnête entre le client et le serveur.
Et voici une idée importante : le résultat du server-mode ne se mesure pas au fait que vous ayez vu « Server started » dans la console, mais au fait que le client externe a reçu la réponse HTTP attendue. Autrement dit, le critère de réussite passe du monde « j'ai lancé quelque chose » au monde « quelqu'un d'autre a obtenu un résultat ».
Pour consolider cette idée, il est utile de voir un schéma simple :
sequenceDiagram
participant C as "Client : Postman/navigateur"
participant A as "ReadLater Starter en mode serveur"
C->>A: "GET /health"
A->>C: "200 OK + JSON"
Sur ce schéma, il n'y a pas de « logique métier », et c'est parfait. Pour l'instant, la mécanique nous importe : la requête est arrivée, le gestionnaire a fonctionné, la réponse a été envoyée.
4. HttpServer du JDK comme pont pédagogique
Il est facile de tomber dans deux extrêmes. Le premier : « puisque JDK a HttpServer, on peut donc écrire la prod comme ça ». Le second : « si ce n'est pas Spring Boot, alors c'est totalement inutile ». Les deux extrêmes sont nuisibles, alors établissons un cadre précis.
HttpServer du JDK (module jdk.httpserver, paquet com.sun.net.httpserver) est l'API serveur minimale intégrée qui permet de démarrer un serveur HTTP sans dépendances externes et sans frameworks. Il n'essaie pas d'être une « pile web complète », et cela nous arrange : nous pouvons lire le code en entier et voir où commence et où finit la magie du serveur. Autrement dit, il n'y aura pas de magie — ce sera « fait à la main ».
Pourquoi est-ce utile avant Spring ? Parce que Spring MVC reprendra ensuite une grande partie de la routine : routage, liaison JSON en DTO, définition des en-têtes, gestion des erreurs, statuts corrects, et bien d'autres choses. Mais si vous n'avez jamais vu à quoi ressemble ce travail fait à la main, Spring sera perçu comme des « incantations avec des annotations ». Notre objectif est qu'après ce cours, l'étudiant pense : « ah, je sais à peu près ce qui se passe à l'intérieur, c'est simplement automatisé maintenant ».
Honnêtement : HttpServer du JDK n'est pas l'outil habituellement choisi pour la production backend. Il a des limitations en termes d'écosystème, de confort, d'extensibilité, et dans les projets réels, les serveurs basés sur Spring Boot (Tomcat/Jetty/Netty), Quarkus, Micronaut, etc. sont plus courants. Mais aujourd'hui, nous ne choisissons pas « la meilleure arme pour la bataille », nous prenons un « simulateur transparent » pour comprendre la mécanique.
5. Critère de succès : /health
Un piège très humain pour un débutant est de penser que si l'application « ne s'est pas plantée » et « est en attente dans le terminal », alors elle fonctionne. En server-mode, ce n'est pas le cas. Le serveur peut démarrer, mais écouter le mauvais port, écouter la mauvaise interface, répondre avec le mauvais content-type, ou planter dès la première requête. Et de l'extérieur, tout cela se ressemble : « quelque chose ne répond pas ».
C'est pourquoi nous fixons à l'avance un résultat clair et mesurable. En fin de journée, nous devons avoir un endpoint :
Chemin : /health
Méthode : GET
Statut : 200 OK
Corps : un petit JSON, par exemple de la forme suivante :
{
"status": "UP",
"appName": "readlater-starter"
}
Remarquez : cette réponse ne concerne pas le domaine de la lecture de livres, ni la liste de lecture, ni le CRUD. Elle est technique. Et c'est précisément pour cela qu'elle est idéale comme point de départ : nous entraînons la mécanique sans nous laisser distraire par le domaine métier.
Et encore une idée importante : le server-mode doit s'appuyer sur ce que nous avons déjà. L'hôte/port sont pris de la configuration, le démarrage du serveur est loggé via SLF4J, le mode de lancement est explicitement indiqué dans les logs. Autrement dit, nous ne « commençons pas une nouvelle application », nous continuons à développer le même ReadLater Starter, simplement dans un nouveau rôle.
La mécanique de démarrage elle-même n'est pas encore l'essentiel. Pour l'instant, il suffit de fixer le critère externe de disponibilité : GET /health doit renvoyer la réponse HTTP attendue. Si le serveur ne répond pas, on regarde d'abord l'adresse et le port, on ne devine pas en lisant le code.
6. Erreurs typiques lors du travail avec le server-mode
À ce stade, les erreurs ne sont presque jamais « complexes », mais « humaines » : on veut voir le résultat rapidement, et donc on casse le cadre architectural soigneux que nous avons nous-mêmes construit ces derniers jours. La bonne nouvelle est que ces erreurs sont faciles à prévenir si l'on comprend à l'avance à quoi elles ressemblent.
Erreur n°1 : faire du server-mode un projet séparé ou une classe main() séparée « pour ne pas gêner ».
Cela semble pratique exactement un soir. Ensuite, vous commencez à dupliquer la configuration, le logging, le mapping JSON, et soudain vous avez deux demi-projets au lieu d'un. Selon le sens du cours, ReadLater Starter doit rester un artefact unique, sinon vous perdez la compétence principale : gérer la croissance de l'application sans sombrer dans le chaos.
Erreur n°2 : cacher le démarrage du serveur dans un if aléatoire avec une chaîne magique.
Une construction du type if ("server".equals(args[0])) { ... } sans mode de lancement normal transforme rapidement le point d'entrée en un ensemble d'aléas. Au bout d'une semaine, vous oublierez vous-même comment lancer l'application correctement, et la logique de démarrage ressemblera à une collection de « solutions temporaires » qui ont mystérieusement survécu à l'auteur.
Erreur n°3 : essayer d'ajouter immédiatement beaucoup de chemins et une « vraie API », en oubliant le critère minimal de disponibilité.
Il est très facile de se laisser emporter et de commencer à dessiner GET /api/v1/reading-list, POST, validation, etc. Mais sans un squelette serveur minimal bien rodé, vous obtiendrez une situation où « rien ne fonctionne, et on ne sait pas pourquoi ». /health est précisément là pour prouver d'abord que le serveur répond en général et que le contrat de réponse est maîtrisé.
Erreur n°4 : considérer comme résultat de la journée que « le serveur a démarré » et ne pas vérifier la réponse HTTP avec un client externe.
Le log « Server started » est une déclaration du serveur sur son propre succès. C'est bien pour lui, bien sûr, mais ce qui nous intéresse, c'est la réalité : ce que voit Postman. Tant qu'une vraie requête HTTP n'a pas été faite et qu'un 200 avec un JSON correct et un Content-Type n'a pas été reçu, il est trop tôt pour considérer la tâche comme accomplie.
Erreur n°5 : revenir à System.out.println() juste au moment où la phase serveur commence.
Paradoxalement, cela arrive souvent : dans la phase client, on a déjà mis en place le logging, mais dans la phase serveur, « bon, je vais vite afficher ça ». Ensuite, dans le log, on ne voit ni le mode, ni l'adresse, ni les requêtes, ni les statuts, et le diagnostic devient une divination. Si nous apprenons l'approche backend, alors le démarrage du serveur et tous les événements importants doivent être loggés correctement.
GO TO FULL VERSION