8. Le serveur RdvMedecins, une boîte noire
Les exemples 22 à 25, puis l'étude de cas, dialoguent avec un serveur : le serveur JSON de l'application [RdvMedecins]. C'est le serveur [NestJS] des cours [React], [Vue.js], [Angular] et [Flutter] : celui du cours [NestJS], dont les contrôleurs ne fabriquent plus des pages HTML mais des réponses JSON. Le lecteur de ces cours connaît déjà ce chapitre, repris ici presque sans changement ; il peut se contenter de la section sur l'authentification, qui explique ce qui change pour une application [Ionic] installée sur un téléphone.
Dans ce cours, ce serveur est une boîte noire : on n'a pas besoin de savoir comment il est écrit. Seule compte son API, c'est-à-dire la liste des requêtes HTTP qu'il accepte et des réponses qu'il renvoie. C'est la situation ordinaire d'un développeur de client : l'API est un contrat, et l'équipe du serveur peut changer tout ce qu'elle veut derrière ce contrat. Son code source est fourni (dossier [rdvmedecins-nestjs-json]) et abondamment commenté, pour le lecteur curieux.
Ce chapitre explique comment installer le serveur, décrit son API, et l'interroge avec [curl].
8.1. L'application RdvMedecins
L'application gère les rendez-vous d'un cabinet médical. Elle a trois rôles :
Rôle | Qui | Ce qu'il peut faire |
[ADMIN] | l'administrateur ([admin]) | prendre et annuler les rendez-vous de tous les patients ; gérer les médecins et les clients |
[DOCTOR] | les médecins ([pelissier], [tatou], [dupont]) | prendre et annuler les rendez-vous de tous les patients ; voir tous les noms dans les agendas |
[USER] | les patients ([martin], [durand], [bistrou], [german]) | réserver pour eux-mêmes ; annuler leur rendez-vous ; gérer leur compte. Dans un agenda, ils ne voient pas le nom des autres patients |
Les comptes de démonstration ont pour mot de passe leur login. Les règles de gestion sont vérifiées par le serveur, quel que soit le client :
- pas de rendez-vous dans le passé ;
- un seul rendez-vous à venir par patient ;
- un créneau n'est réservé qu'une fois par jour ;
- deux médecins (ou deux clients) ne portent pas les mêmes prénom et nom ; un login n'appartient qu'à un compte ;
- un médecin qui a des rendez-vous ne peut pas être supprimé ;
- une fiche modifiée entre-temps par quelqu'un d'autre ne peut pas être écrasée (verrou optimiste) ;
- un patient ne reçoit jamais le nom d'un autre patient.
8.2. Installation
Il faut [Node.js] (déjà installé pour [Angular], cf. chapitre 2) et un serveur [MySQL] (ou [MariaDB]) : leur installation est décrite en annexe.
1. La base de données. Le script [database/dbrdvmedecins2.sql] crée la base [dbrdvmedecins2], ses tables et ses données de démonstration. Avec [Laragon], on l'exécute dans [HeidiSQL] (menu Fichier / Exécuter un fichier SQL) ; en ligne de commande :
| mysql -u root -p < database/dbrdvmedecins2.sql
|
Le script repart toujours d'une base vide : on peut le relancer pour retrouver les données de départ.
2. Les bibliothèques. Dans une console ouverte sur le dossier [rdvmedecins-nestjs-json] :
3. La configuration. On copie le fichier [.env.example] en [.env] (sous [Windows] : [copy .env.example .env]), puis on l'adapte :
| # -----------------------------------------------------------------------
# Fichier d'exemple de configuration.
# Copier ce fichier en [.env] puis adapter les valeurs à votre machine.
# Le fichier [.env] ne doit jamais être partagé ou commité (mots de passe).
# -----------------------------------------------------------------------
# port d'écoute du serveur JSON
PORT=8080
# paramètres de connexion à la base de données MySQL [dbrdvmedecins2]
# (script : database/dbrdvmedecins2.sql)
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=
DB_DATABASE=dbrdvmedecins2
# true : affiche dans la console les requêtes SQL envoyées à MySQL
DB_LOGGING=false
# -----------------------------------------------------------------------
# authentification
# -----------------------------------------------------------------------
# clé secrète utilisée pour signer/vérifier les jetons JWT - à changer
# impérativement en production (une longue chaîne aléatoire, jamais commitée).
JWT_SECRET=change-moi-en-une-longue-chaine-aleatoire
# durée de validité d'un jeton JWT, en secondes (7200 s = 2 h). Le cookie
# qui transporte le jeton a la même durée de vie.
JWT_EXPIRES_IN_SECONDS=7200
# mettre à true en production (site servi en HTTPS) : le cookie
# d'authentification ne sera alors jamais envoyé sur une connexion non chiffrée.
COOKIE_SECURE=false
# -----------------------------------------------------------------------
# protection contre les robots
# -----------------------------------------------------------------------
# connexion : nombre d'échecs (mauvais captcha ou mot de passe) autorisés pour
# un même login depuis une même adresse IP, avant blocage de ce couple...
LOGIN_MAX_ECHECS=5
# ... pendant cette durée (une adresse IP est aussi bloquée si elle cumule
# 5 fois plus d'échecs, tous logins confondus)
LOGIN_BLOCAGE_MINUTES=15
# création de compte client : nombre maximal de comptes créés par heure
# depuis une même adresse IP. En salle de cours, tous les postes sortent
# souvent par la même adresse : augmenter si besoin.
INSCRIPTION_MAX_PAR_HEURE=10
# MODE TEST du captcha : si cette variable a une valeur, cette valeur est
# acceptée comme réponse à N'IMPORTE QUEL captcha. Pratique pour interroger
# l'API avec curl (on ne peut pas "lire" l'image dans une console).
# À laisser VIDE en production.
CAPTCHA_TEST=
# -----------------------------------------------------------------------
# production
# -----------------------------------------------------------------------
# dossier du client web compilé, servi par ce serveur (par défaut : client/)
# DOSSIER_CLIENT=client
# mettre à true si le serveur est derrière un proxy (nginx...)
TRUST_PROXY=false
|
- ligne 8 : le serveur écoute sur le port 8080 ;
- lignes 12-16 : la connexion à [MySQL]. Avec [Laragon], l'utilisateur [root] n'a pas de mot de passe : les valeurs par défaut conviennent ;
- ligne 25 : la clé secrète qui signe les jetons d'authentification ;
- lignes 38-45 : les protections contre les robots : au bout de 5 échecs de connexion pour un même login depuis une même adresse IP, ce couple est bloqué 15 minutes ; une même adresse IP ne crée pas plus de 10 comptes par heure. Pendant la mise au point d'une application, où l'on se trompe souvent, on peut relever ces valeurs dans son fichier [.env] ;
- ligne 51 : le mode test du captcha. La page de connexion demande de recopier les caractères d'une image, ce qui est impossible depuis une console. Si cette variable a une valeur, cette valeur est acceptée comme réponse à n'importe quel captcha, comme les « clés de test » que [Google] fournit pour son [reCAPTCHA]. Pour les essais avec [curl] de ce chapitre, on écrit [CAPTCHA_TEST=test]. Les scripts qui ont fabriqué les copies d'écran de ce cours s'en servent aussi. On la laisse VIDE en production ;
- lignes 57-59 : deux réglages pour la production (cf. chapitre « Déploiement »).
4. Le lancement.
La console affiche (extrait ; [start:dev] relance le serveur à chaque modification de son code) :
| [Nest] 18372 - 10/06/2026, 1:29:56 PM LOG [NestFactory] Starting Nest application...
[Nest] 18372 - 10/06/2026, 1:29:56 PM LOG [InstanceLoader] TypeOrmModule dependencies initialized +228ms
...
[Nest] 18372 - 10/06/2026, 1:29:56 PM WARN [RdvMedecins] MODE TEST du captcha actif (CAPTCHA_TEST dans .env) : à ne jamais utiliser en production
[Nest] 18372 - 10/06/2026, 1:29:56 PM LOG [RoutesResolver] AgendaController {/api/agenda}: +4ms
[Nest] 18372 - 10/06/2026, 1:29:56 PM LOG [RouterExplorer] Mapped {/api/agenda, GET} route +4ms
[Nest] 18372 - 10/06/2026, 1:29:56 PM LOG [RoutesResolver] RendezVousController {/api/rendez-vous}: +0ms
...
[Nest] 18372 - 10/06/2026, 1:29:56 PM LOG [RouterExplorer] Mapped {/api/auth/moi, GET} route +0ms
[Nest] 18372 - 10/06/2026, 1:29:56 PM LOG [NestApplication] Nest application successfully started +22ms
[Nest] 18372 - 10/06/2026, 1:29:56 PM LOG [RdvMedecins] serveur JSON démarré : http://localhost:8080/api
|
Chaque ligne [Mapped] annonce une route de l'API. Si la base est inaccessible (serveur [MySQL] arrêté, mauvais mot de passe), le serveur s'arrête aussitôt avec un message d'erreur de connexion.
Toutes les routes commencent par [/api]. Une requête réussie reçoit un code 2xx et, dans son corps, les données demandées, en JSON. Une requête refusée reçoit un code 4xx ou 5xx et, TOUJOURS, un corps de la même forme :
| {
"statusCode": 400,
"cle": "ERRORS.INVALID_FORM",
"params": { "min": "6" },
"champs": { "nom": "ERRORS.LASTNAME_REQUIRED", "password": "ERRORS.PASSWORD_TOO_SHORT" }
}
|
- [statusCode] : le code HTTP, répété dans le corps ;
- [cle] : la clé de traduction du message. Le serveur ne renvoie jamais de texte à afficher : il ne connaît pas la langue de l'utilisateur. C'est le client qui traduit la clé avec ses dictionnaires (cf. exemple 21) ;
- [params] (facultatif) : les paramètres des messages (« Au moins {{min}} caractères », « Réessayez dans {{minutes}} minute(s) ») ;
- [champs] (facultatif) : pour un formulaire refusé, l'erreur de chaque champ.
Les codes utilisés :
Code | Signification | Exemples de clés |
400 | requête invalide | [ERRORS.INVALID_FORM] (+ champs), [ERRORS.INVALID_DATE], [ERRORS.WRONG_CAPTCHA], [ERRORS.CHOOSE_PATIENT], [ERRORS.WRONG_CURRENT_PASSWORD], [ERRORS.BAD_REQUEST]... |
401 | pas authentifié | [ERRORS.AUTH_REQUIRED] (pas de jeton), [ERRORS.SESSION_EXPIRED] (jeton invalide ou expiré), [LOGIN.ERROR] |
403 | accès refusé | [ERRORS.FORBIDDEN] (rôle insuffisant), [ERRORS.CAPTCHA_BEFORE_ACCOUNT] (pas de laissez-passer) |
404 | introuvable | [ERRORS.NOT_FOUND], [ERRORS.DOCTOR_NOT_FOUND], [ERRORS.APPOINTMENT_NOT_FOUND]... |
409 | règle de gestion violée | [ERRORS.PAST_DATE], [ERRORS.SLOT_ALREADY_BOOKED], [ERRORS.LOGIN_TAKEN], [ERRORS.CONCURRENT_UPDATE]... |
415 | POST qui n'est pas en JSON | [ERRORS.JSON_REQUIRED] |
429 | trop de tentatives | [ERRORS.TOO_MANY_ATTEMPTS], [ERRORS.TOO_MANY_ACCOUNTS] (+ params.minutes) |
500 | anomalie du serveur | [ERRORS.SERVER_ERROR] |
8.4. L'authentification
Le client s'authentifie par [POST /api/auth/login], avec login, mot de passe et réponse au captcha. En retour, le serveur envoie, dans l'en-tête [Set-Cookie] de sa réponse, un cookie qui contient un jeton JWT signé :
| Set-Cookie: rdvmedecins_jwt=eyJhbGciOiJIUzI1NiIs...; Max-Age=7200; Path=/api; Expires=Tue, 06 Oct 2026 14:12:15 GMT; HttpOnly; SameSite=Strict
|
Le client doit ensuite présenter ce cookie, dans l'en-tête [Cookie], à chaque requête vers [/api] : c'est ainsi que le serveur sait qui l'interroge.
Ce serveur a été écrit pour des clients web, et ses choix s'expliquent par les dangers propres aux navigateurs :
- [HttpOnly] : le code [JavaScript] de la page ne peut PAS lire ce cookie. Un script malveillant qui aurait été injecté dans la page ne pourrait donc pas voler le jeton. Conséquence pour le client : il ne sait pas, par lui-même, s'il est connecté. Il le demande au serveur ([GET /api/auth/moi]) ;
- [Path=/api] : le cookie n'est joint qu'aux requêtes vers l'API ;
- [SameSite=Strict] : le navigateur ne joint le cookie qu'aux requêtes émises DEPUIS une page de ce site. Une page piégée sur un autre site ne peut pas faire agir le navigateur au nom de l'utilisateur (attaque dite CSRF) ;
- [Max-Age=7200] : il disparaît au bout de deux heures, en même temps que le jeton expire.
Une seconde protection contre les attaques CSRF : le serveur refuse (415) toute requête POST dont le corps n'est pas en JSON. Un formulaire HTML piégé, sur un autre site, ne sait pas envoyer de JSON.
8.4.1. Ce qui change pour un client mobile
Une application [Ionic] est une application web, exécutée sur le téléphone par une « WebView » (le moteur du navigateur [Chrome] d'[Android]) : a-t-elle les mêmes règles qu'un navigateur ? Il faut distinguer les deux façons dont elle tourne dans ce cours.
Dans le navigateur de l'ordinateur, pendant le développement ([ng serve]). L'application est servie par le serveur de développement d'[Angular] (http://localhost:8100) ; le serveur JSON est sur un autre port (8080). Ce sont deux origines différentes, et le navigateur appliquerait deux règles gênantes : il ne lirait pas les réponses d'une autre origine, sauf autorisation du serveur (le mécanisme « CORS », que notre serveur n'offre pas), et il ne joindrait pas le cookie [SameSite=Strict] aux requêtes « d'un autre site ». La solution est la même que dans les cours [React] et [Vue.js] : un proxy. Le serveur de développement transmet au serveur JSON toute requête dont l'adresse commence par [/api] (fichier [proxy.conf.json], cf. exemple 22). Pour le navigateur, l'application et l'API ont la même origine : il range le cookie et le renvoie, et le code de l'application n'a rien à faire.
Sur le téléphone ([Android], [Capacitor]). La page est servie par [Capacitor] lui-même, depuis l'adresse https://localhost ; le serveur JSON est sur l'ordinateur, à une adresse comme http://10.0.2.2:8080 (cf. exemple 22). Deux origines, là encore. Le projet active le greffon [CapacitorHttp] de [Capacitor] (fichier [capacitor.config.ts]) : les requêtes HTTP de l'application ne sont plus envoyées par la WebView mais par le code natif d'[Android]. Conséquences :
- il n'y a plus de contrôle CORS : ce contrôle est une règle des navigateurs, et le code natif n'en est pas un (pas plus que [curl]) ;
- les cookies reçus sont rangés par [Android] (son gestionnaire de cookies, le même que celui de la WebView), et renvoyés automatiquement aux requêtes suivantes vers le même serveur. Ils sont conservés sur l'appareil : on reste connecté quand on arrête puis relance l'application ;
- les attributs [HttpOnly] et [SameSite] ne gênent pas : le code de l'application ne lit jamais le cookie (il demande à [GET /api/auth/moi] qui est connecté), et la requête n'est pas émise par une page « d'un autre site ».
C'est une différence importante avec le cours [Flutter] : une application [Flutter] n'a pas de navigateur, et devait écrire elle-même un client HTTP qui lit l'en-tête [Set-Cookie] des réponses et écrit l'en-tête [Cookie] des requêtes. Avec [Ionic], dans le navigateur comme sur le téléphone, le cookie circule sans une ligne de code. Le serveur, lui, n'a rien à changer : il ne voit que des requêtes HTTP ordinaires, qui présentent le cookie qu'il a déposé.
8.5. L'API
Méthode | Adresse | Rôles | Corps envoyé / réponse |
GET | [/api/captcha] | public | réponse : [{ id, svg }] |
POST | [/api/auth/login] | public | [{ login, password, captchaId, captcha }] ; réponse : [{ login, nom, role }] + cookie |
POST | [/api/auth/logout] | public | [{}] ; efface le cookie |
GET | [/api/auth/moi] | connecté | réponse : [{ login, nom, role }] |
GET | [/api/agenda?idMedecin=&jour=] | tous | réponse : l'agenda (sans idMedecin : pour un DOCTOR, le sien) |
POST | [/api/rendez-vous] | tous | [{ jour, idCreneau, idClient }] (idClient ignoré pour un USER) ; 201 |
DELETE | [/api/rendez-vous/:id] | tous | un USER n'annule que le sien |
GET | [/api/medecins] | tous | la liste (avec login pour l'ADMIN) |
GET | [/api/clients] | ADMIN, DOCTOR | la liste (avec login pour l'ADMIN) |
GET | [/api/medecins/:id], [/api/clients/:id] | ADMIN | une fiche |
POST | [/api/medecins], [/api/clients] | ADMIN | [{ titre, prenom, nom, login?, password? }] ; 201 |
PUT | [/api/medecins/:id], [/api/clients/:id] | ADMIN | [{ titre, prenom, nom, login?, password?, version }] |
DELETE | [/api/medecins/:id], [/api/clients/:id] | ADMIN | réponse : [{}] |
POST | [/api/inscription/acces] | public | [{ captchaId, captcha }] ; délivre un laissez-passer (cookie) |
GET | [/api/inscription/acces] | public | réponse : [{ valide }] |
POST | [/api/inscription] | public + laissez-passer | [{ titre, prenom, nom, login, password, confirmation }] ; 201 |
GET | [/api/mon-espace] | USER | réponse : [{ patient, rendezVous }] |
GET | [/api/mon-compte] | USER | réponse : [{ titre, prenom, nom, login, version }] |
PUT | [/api/mon-compte] | USER | [{ titre, prenom, nom, login, password?, confirmation?, motDePasseActuel, version }] |
DELETE | [/api/mon-compte] | USER | [{ motDePasseActuel }] ; supprime le compte et ses rendez-vous |
Une ligne de l'agenda ([GET /api/agenda]) décrit un créneau ET ce que l'utilisateur connecté a le droit d'en faire :
| { "id": 3, "hdebut": 8, "mdebut": 40, "hfin": 9, "mfin": 0,
"libre": false,
"patient": { "titre": "Mme", "prenom": "Alice", "nom": "DURAND" },
"moi": false, "reservable": false, "idRv": 2 }
|
- [libre] : le créneau est-il libre ce jour-là ?
- [patient] : le patient du rendez-vous. Pour un patient connecté, il vaut [null] sauf pour SON rendez-vous : le nom des autres patients n'est jamais envoyé ;
- [moi] : c'est le rendez-vous du patient connecté ;
- [reservable] : l'utilisateur peut-il réserver ce créneau (libre, pas dans le passé, et, pour un patient, pas de rendez-vous déjà à venir) ?
- [idRv] : renseigné si l'utilisateur peut annuler ce rendez-vous.
C'est le serveur qui DÉCIDE ; le client affiche. Et même si un client trichait (en affichant un bouton « Réserver » qu'il ne devrait pas), le serveur revérifierait tout au moment de la réservation.
8.6. Interroger l'API avec curl
Le dossier [curl] du serveur contient les corps JSON des requêtes de ce chapitre, un fichier par requête. Par exemple, [curl/login-admin.json] :
| { "login": "admin", "password": "admin", "captchaId": "-", "captcha": "test" }
|
Les commandes se tapent dans une console ouverte sur le dossier [rdvmedecins-nestjs-json], le serveur étant lancé en mode test du captcha ([CAPTCHA_TEST=test]), sur une base qui vient d'être (ré)initialisée. Quelques options de [curl] :
- [-i] : afficher aussi les en-têtes de la réponse ; [-s] : sans barre de progression ;
- [--json @fichier] : envoyer le contenu du fichier en POST, avec l'en-tête [Content-Type: application/json] ([curl] 7.82 et plus ; avec une version plus ancienne, on écrit [-H "Content-Type: application/json" -d @fichier]) ;
- [-X PUT], [-X DELETE] : la méthode HTTP ;
- [-c cookies.txt] : enregistrer les cookies reçus dans ce fichier ; [-b cookies.txt] : les renvoyer. C'est ainsi que [curl] « reste connecté » d'une commande à l'autre, comme le ferait un navigateur.
Sous [PowerShell], il faut taper [curl.exe] et non [curl] (qui y désigne une autre commande). Les sorties qui suivent sont celles d'une session réelle (le 6 octobre 2026, avec des rendez-vous pris en janvier 2030, pour qu'ils soient toujours à venir). Les réponses JSON un peu longues ont été mises en forme pour ce document (une ligne par objet), les jetons JWT et l'image du captcha tronqués (« ... »), et les en-têtes [Date], [ETag], [Connection] et [Keep-Alive] retirés.
8.6.1. Connexion
Le captcha d'abord (l'image est tronquée) :
| $ curl -i http://localhost:8080/api/captcha
HTTP/1.1 200 OK
Cache-Control: no-store
Content-Type: application/json; charset=utf-8
Content-Length: 13504
{"id":"3f533304-2b91-43eb-b6b8-23764f54afe0","svg":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"150..."}
|
Sans jeton, une route protégée répond 401 :
| $ curl -i http://localhost:8080/api/medecins
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Content-Length: 47
{"statusCode":401,"cle":"ERRORS.AUTH_REQUIRED"}
|
Un mauvais captcha, puis un mauvais mot de passe :
| $ curl -i --json @curl/login-mauvais-captcha.json http://localhost:8080/api/auth/login
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 91
{"statusCode":400,"cle":"ERRORS.WRONG_CAPTCHA","champs":{"captcha":"ERRORS.WRONG_CAPTCHA"}}
|
| $ curl -i --json @curl/login-mauvais-mdp.json http://localhost:8080/api/auth/login
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Content-Length: 38
{"statusCode":401,"cle":"LOGIN.ERROR"}
|
La connexion réussie dépose le cookie, que [curl] range dans [cookies.txt] :
| $ curl -i -c cookies.txt --json @curl/login-admin.json http://localhost:8080/api/auth/login
HTTP/1.1 200 OK
Set-Cookie: rdvmedecins_jwt=eyJhbGciOiJIUzI1NiIs...; Max-Age=7200; Path=/api; Expires=Tue, 06 Oct 2026 14:12:15 GMT; HttpOnly; SameSite=Strict
Content-Type: application/json; charset=utf-8
Content-Length: 55
{"login":"admin","nom":"Administrateur","role":"ADMIN"}
|
Avec ce cookie, le serveur sait qui nous sommes :
| $ curl -i -b cookies.txt http://localhost:8080/api/auth/moi
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 55
{"login":"admin","nom":"Administrateur","role":"ADMIN"}
|
Un jeton falsifié (ou expiré) donne une session expirée, et le serveur demande d'effacer le cookie (une date d'expiration passée) :
| $ curl -i -b "rdvmedecins_jwt=abc.def.ghi" http://localhost:8080/api/auth/moi
HTTP/1.1 401 Unauthorized
Set-Cookie: rdvmedecins_jwt=; Path=/api; Expires=Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 49
{"statusCode":401,"cle":"ERRORS.SESSION_EXPIRED"}
|
8.6.2. Médecins, clients, agenda
La liste des médecins, vue par l'ADMIN (avec les logins) :
| $ curl -i -b cookies.txt http://localhost:8080/api/medecins
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 258
[
{"id":3,"version":0,"titre":"Melle","prenom":"Sophie","nom":"DUPONT","login":"dupont"},
{"id":1,"version":0,"titre":"Mme","prenom":"Marie","nom":"PELISSIER","login":"pelissier"},
{"id":2,"version":0,"titre":"Mr","prenom":"Paul","nom":"TATOU","login":"tatou"}
]
|
L'agenda du docteur PELISSIER le 15 janvier 2030 :
| $ curl -s -b cookies.txt "http://localhost:8080/api/agenda?idMedecin=1&jour=2030-01-15"
{
"medecin":{"id":1,"version":0,"titre":"Mme","prenom":"Marie","nom":"PELISSIER"},
"jour":"2030-01-15",
"creneaux":[
{"id":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":2,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":3,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":4,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":5,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":6,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":7,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":8,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":9,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":10,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":11,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":12,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null}
],
"info":null
}
|
Une date qui n'existe pas :
| $ curl -i -b cookies.txt "http://localhost:8080/api/agenda?idMedecin=1&jour=2030-02-30"
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 46
{"statusCode":400,"cle":"ERRORS.INVALID_DATE"}
|
8.6.3. Rendez-vous
L'ADMIN réserve le créneau 3 pour la patiente n° 2 (Alice DURAND). Le corps de la requête ([curl/rv-admin.json]) :
| { "jour": "2030-01-15", "idCreneau": 3, "idClient": 2 }
|
| $ curl -i -b cookies.txt --json @curl/rv-admin.json http://localhost:8080/api/rendez-vous
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 55
{"id":2,"jour":"2030-01-15","idCreneau":3,"idClient":2}
|
L'agenda montre maintenant ce rendez-vous, avec le nom de la patiente et le moyen de l'annuler ([idRv]) :
| $ curl -s -b cookies.txt "http://localhost:8080/api/agenda?idMedecin=1&jour=2030-01-15"
{
"medecin":{"id":1,"version":0,"titre":"Mme","prenom":"Marie","nom":"PELISSIER"},
"jour":"2030-01-15",
"creneaux":[
{"id":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":2,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":3,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"libre":false,"patient":{"titre":"Mme","prenom":"Alice","nom":"DURAND"},"moi":false,"reservable":false,"idRv":2},
{"id":4,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":5,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":6,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":7,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":8,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":9,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":10,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":11,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":12,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null}
],
"info":null
}
|
Recommencer : la patiente a déjà un rendez-vous à venir. Un rendez-vous dans le passé (le 15 janvier 2020, fichier [curl/rv-passe.json]) est refusé lui aussi :
| $ curl -i -b cookies.txt --json @curl/rv-admin.json http://localhost:8080/api/rendez-vous
HTTP/1.1 409 Conflict
Content-Type: application/json; charset=utf-8
Content-Length: 66
{"statusCode":409,"cle":"ERRORS.PATIENT_HAS_UPCOMING_APPOINTMENT"}
|
| $ curl -i -b cookies.txt --json @curl/rv-passe.json http://localhost:8080/api/rendez-vous
HTTP/1.1 409 Conflict
Content-Type: application/json; charset=utf-8
Content-Length: 43
{"statusCode":409,"cle":"ERRORS.PAST_DATE"}
|
Un formulaire HTML ordinaire (ici simulé par [-d], qui envoie [application/x-www-form-urlencoded]) est refusé :
| $ curl -i -b cookies.txt -d "jour=2030-01-15&idCreneau=4&idClient=2" http://localhost:8080/api/rendez-vous
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json; charset=utf-8
Content-Length: 47
{"statusCode":415,"cle":"ERRORS.JSON_REQUIRED"}
|
8.6.4. Le point de vue d'un patient
On se déconnecte, puis l'on se connecte comme [martin] :
| $ curl -i -b cookies.txt -c cookies.txt --json @curl/vide.json http://localhost:8080/api/auth/logout
HTTP/1.1 200 OK
Set-Cookie: rdvmedecins_jwt=; Path=/api; Expires=Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2
{}
|
| $ curl -s -c cookies-martin.txt --json @curl/login-martin.json http://localhost:8080/api/auth/login
{"login":"martin","nom":"Jean MARTIN","role":"USER"}
|
Le même agenda, vu par un patient : le créneau 3 est occupé (["libre":false]), mais ["patient"] vaut [null] : le nom d'Alice DURAND n'a pas été envoyé :
| $ curl -s -b cookies-martin.txt "http://localhost:8080/api/agenda?idMedecin=1&jour=2030-01-15"
{
"medecin":{"id":1,"version":0,"titre":"Mme","prenom":"Marie","nom":"PELISSIER"},
"jour":"2030-01-15",
"creneaux":[
{"id":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":2,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":3,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"libre":false,"patient":null,"moi":false,"reservable":false,"idRv":null},
{"id":4,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":5,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":6,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":7,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":8,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":9,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":10,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":11,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":12,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null}
],
"info":null
}
|
Un patient n'a pas accès à la liste des clients :
| $ curl -i -b cookies-martin.txt http://localhost:8080/api/clients
HTTP/1.1 403 Forbidden
Content-Type: application/json; charset=utf-8
Content-Length: 43
{"statusCode":403,"cle":"ERRORS.FORBIDDEN"}
|
Il réserve pour lui-même (le corps, [curl/rv-martin.json], ne contient pas d'[idClient]), une fois, pas deux ([curl/rv-martin-2.json] : le lendemain, créneau 6) :
| { "jour": "2030-01-15", "idCreneau": 5 }
|
| $ curl -i -b cookies-martin.txt --json @curl/rv-martin.json http://localhost:8080/api/rendez-vous
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 55
{"id":3,"jour":"2030-01-15","idCreneau":5,"idClient":1}
|
| $ curl -i -b cookies-martin.txt --json @curl/rv-martin-2.json http://localhost:8080/api/rendez-vous
HTTP/1.1 409 Conflict
Content-Type: application/json; charset=utf-8
Content-Length: 53
{"statusCode":409,"cle":"AGENDA.ALREADY_BOOKED_INFO"}
|
Son espace :
| $ curl -s -b cookies-martin.txt http://localhost:8080/api/mon-espace
{
"patient":{"titre":"Mr","prenom":"Jean","nom":"MARTIN"},
"rendezVous":[
{
"id":3,
"jour":"2030-01-15",
"creneau":{"id":5,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40},
"medecin":{"id":1,"version":0,"titre":"Mme","prenom":"Marie","nom":"PELISSIER"}
}
]
}
|
Il ne peut pas annuler le rendez-vous d'un autre patient. Le serveur répond « introuvable », exactement comme pour un rendez-vous qui n'existe pas : il ne révèle rien des rendez-vous des autres :
| $ curl -i -b cookies-martin.txt -X DELETE http://localhost:8080/api/rendez-vous/2
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Content-Length: 55
{"statusCode":404,"cle":"ERRORS.APPOINTMENT_NOT_FOUND"}
|
Le sien, oui :
| $ curl -i -b cookies-martin.txt -X DELETE http://localhost:8080/api/rendez-vous/3
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 2
{}
|
8.6.5. Le point de vue d'un médecin
Le docteur PELISSIER se connecte :
| $ curl -s -c cookies-pelissier.txt --json @curl/login-pelissier.json http://localhost:8080/api/auth/login
{"login":"pelissier","nom":"Marie PELISSIER","role":"DOCTOR"}
|
Sans [idMedecin], un médecin connecté reçoit SON agenda, avec tous les noms (le rendez-vous de [martin] vient d'être annulé : reste celui d'Alice DURAND) :
| $ curl -s -b cookies-pelissier.txt "http://localhost:8080/api/agenda?jour=2030-01-15"
{
"medecin":{"id":1,"version":0,"titre":"Mme","prenom":"Marie","nom":"PELISSIER"},
"jour":"2030-01-15",
"creneaux":[
{"id":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":2,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":3,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"libre":false,"patient":{"titre":"Mme","prenom":"Alice","nom":"DURAND"},"moi":false,"reservable":false,"idRv":2},
{"id":4,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":5,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":6,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":7,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":8,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":9,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":10,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":11,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null},
{"id":12,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"libre":true,"patient":null,"moi":false,"reservable":true,"idRv":null}
],
"info":null
}
|
Il ne gère pas les médecins :
| $ curl -i -b cookies-pelissier.txt --json @curl/medecin-ajout.json http://localhost:8080/api/medecins
HTTP/1.1 403 Forbidden
Content-Type: application/json; charset=utf-8
Content-Length: 43
{"statusCode":403,"cle":"ERRORS.FORBIDDEN"}
|
8.6.6. La gestion des médecins et des clients (ADMIN)
L'ADMIN se reconnecte (il s'était déconnecté plus haut) :
| $ curl -s -c cookies.txt --json @curl/login-admin.json http://localhost:8080/api/auth/login
{"login":"admin","nom":"Administrateur","role":"ADMIN"}
|
Un ajout. Le login, absent, vaut par défaut le nom en minuscules ; le mot de passe, le login ([curl/medecin-ajout.json]) :
| { "titre": "Mme", "prenom": "Claire", "nom": "LEROY" }
|
| $ curl -i -b cookies.txt --json @curl/medecin-ajout.json http://localhost:8080/api/medecins
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 82
{"id":4,"version":1,"titre":"Mme","prenom":"Claire","nom":"LEROY","login":"leroy"}
|
Un corps invalide : la réponse donne l'erreur de chaque champ ([curl/medecin-invalide.json]) :
| { "titre": "Dr", "prenom": "", "nom": "UN-NOM-BIEN-TROP-LONG-POUR-LA-BASE", "login": "Claire L" }
|
| $ curl -i -b cookies.txt --json @curl/medecin-invalide.json http://localhost:8080/api/medecins
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 210
{
"statusCode":400,
"cle":"ERRORS.INVALID_FORM",
"params":{"min":"6"},
"champs":{"login":"ERRORS.INVALID_LOGIN","titre":"ERRORS.INVALID_TITLE","prenom":"ERRORS.FIRSTNAME_REQUIRED","nom":"ERRORS.LASTNAME_TOO_LONG"}
}
|
Un homonyme (Mr Paul TATOU, [curl/medecin-doublon.json]) : les deux champs concernés sont signalés :
| $ curl -i -b cookies.txt --json @curl/medecin-doublon.json http://localhost:8080/api/medecins
HTTP/1.1 409 Conflict
Content-Type: application/json; charset=utf-8
Content-Length: 143
{
"statusCode":409,
"cle":"ERRORS.DOCTOR_ALREADY_EXISTS",
"champs":{"prenom":"ERRORS.DOCTOR_ALREADY_EXISTS","nom":"ERRORS.DOCTOR_ALREADY_EXISTS"}
}
|
Une modification. La [version] est celle lue avec la fiche (ici 1, [curl/medecin-modif.json]) :
| { "titre": "Mme", "prenom": "Claire", "nom": "LEROY-MARTIN", "version": 1 }
|
| $ curl -i -b cookies.txt -X PUT --json @curl/medecin-modif.json http://localhost:8080/api/medecins/4
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 89
{"id":4,"version":2,"titre":"Mme","prenom":"Claire","nom":"LEROY-MARTIN","login":"leroy"}
|
La fiche est maintenant en version 2. Renvoyer la même modification (avec la version 1) revient à écraser une fiche modifiée entre-temps par quelqu'un d'autre : c'est le verrou optimiste qui refuse :
| $ curl -i -b cookies.txt -X PUT --json @curl/medecin-modif.json http://localhost:8080/api/medecins/4
HTTP/1.1 409 Conflict
Content-Type: application/json; charset=utf-8
Content-Length: 51
{"statusCode":409,"cle":"ERRORS.CONCURRENT_UPDATE"}
|
Un médecin qui a des rendez-vous ne peut pas être supprimé ; les autres, si :
| $ curl -i -b cookies.txt -X DELETE http://localhost:8080/api/medecins/1
HTTP/1.1 409 Conflict
Content-Type: application/json; charset=utf-8
Content-Length: 57
{"statusCode":409,"cle":"ERRORS.DOCTOR_HAS_APPOINTMENTS"}
|
| $ curl -i -b cookies.txt -X DELETE http://localhost:8080/api/medecins/4
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 2
{}
|
8.6.7. La création d'un compte
Sans laissez-passer, impossible :
| $ curl -i --json @curl/inscription.json http://localhost:8080/api/inscription
HTTP/1.1 403 Forbidden
Content-Type: application/json; charset=utf-8
Content-Length: 56
{"statusCode":403,"cle":"ERRORS.CAPTCHA_BEFORE_ACCOUNT"}
|
On résout d'abord un captcha ([curl/captcha-test.json] : [{ "captchaId": "-", "captcha": "test" }]) ; le serveur délivre un laissez-passer, valable 15 minutes, dans un cookie :
| $ curl -i -c cookies-inscription.txt --json @curl/captcha-test.json http://localhost:8080/api/inscription/acces
HTTP/1.1 200 OK
Set-Cookie: rdvmedecins_inscription=6bfb75fc-1017-41ea-abca-cc2e8f0c253b; Max-Age=900; Path=/api/inscription; Expires=Tue, 06 Oct 2026 12:27:16 GMT; HttpOnly; SameSite=Strict
Content-Type: application/json; charset=utf-8
Content-Length: 2
{}
|
| $ curl -s -b cookies-inscription.txt http://localhost:8080/api/inscription/acces
{"valide":true}
|
La confirmation du mot de passe doit être identique ([curl/inscription-confirmation.json] : « secret1 », puis « secret2 ») :
| $ curl -i -b cookies-inscription.txt --json @curl/inscription-confirmation.json http://localhost:8080/api/inscription
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 98
{"statusCode":400,"cle":"ERRORS.INVALID_FORM","champs":{"confirmation":"ERRORS.PASSWORDS_DIFFER"}}
|
Le corps correct ([curl/inscription.json]) :
| { "titre": "Mme", "prenom": "Julie", "nom": "BERNARD", "login": "jbernard", "password": "secret1", "confirmation": "secret1" }
|
| $ curl -i -b cookies-inscription.txt -c cookies-inscription.txt --json @curl/inscription.json http://localhost:8080/api/inscription
HTTP/1.1 201 Created
Set-Cookie: rdvmedecins_inscription=; Path=/api/inscription; Expires=Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 20
{"login":"jbernard"}
|
Le laissez-passer a servi : il ne permet pas de créer un second compte :
| $ curl -i -b cookies-inscription.txt --json @curl/inscription.json http://localhost:8080/api/inscription
HTTP/1.1 403 Forbidden
Content-Type: application/json; charset=utf-8
Content-Length: 56
{"statusCode":403,"cle":"ERRORS.CAPTCHA_BEFORE_ACCOUNT"}
|
8.6.8. Gérer son compte
La nouvelle patiente se connecte et lit sa fiche :
| $ curl -s -c cookies-jbernard.txt --json @curl/login-jbernard.json http://localhost:8080/api/auth/login
{"login":"jbernard","nom":"Julie BERNARD","role":"USER"}
|
| $ curl -s -b cookies-jbernard.txt http://localhost:8080/api/mon-compte
{"titre":"Mme","prenom":"Julie","nom":"BERNARD","login":"jbernard","version":1}
|
Une modification exige le mot de passe actuel. Le serveur délivre un nouveau jeton, qui contient le nouveau nom ([curl/mon-compte.json]) :
| { "titre": "Mme", "prenom": "Julie", "nom": "BERNARD-DUVAL", "login": "jbernard", "motDePasseActuel": "secret1", "version": 1 }
|
| $ curl -i -b cookies-jbernard.txt -c cookies-jbernard.txt -X PUT --json @curl/mon-compte.json http://localhost:8080/api/mon-compte
HTTP/1.1 200 OK
Set-Cookie: rdvmedecins_jwt=eyJhbGciOiJIUzI1NiIs...; Max-Age=7200; Path=/api; Expires=Tue, 06 Oct 2026 14:12:17 GMT; HttpOnly; SameSite=Strict
Content-Type: application/json; charset=utf-8
Content-Length: 62
{"login":"jbernard","nom":"Julie BERNARD-DUVAL","role":"USER"}
|
La suppression du compte ([curl/mon-compte-suppression.json] : [{ "motDePasseActuel": "secret1" }]) efface aussi le cookie :
| $ curl -i -b cookies-jbernard.txt -c cookies-jbernard.txt -X DELETE --json @curl/mon-compte-suppression.json http://localhost:8080/api/mon-compte
HTTP/1.1 200 OK
Set-Cookie: rdvmedecins_jwt=; Path=/api; Expires=Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2
{}
|
| $ curl -i -b cookies-jbernard.txt http://localhost:8080/api/auth/moi
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Content-Length: 47
{"statusCode":401,"cle":"ERRORS.AUTH_REQUIRED"}
|
8.6.9. Quelques erreurs
Une route inconnue, un corps qui n'est pas du JSON valide :
| $ curl -i -b cookies-martin.txt http://localhost:8080/api/n-existe-pas
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Content-Length: 43
{"statusCode":404,"cle":"ERRORS.NOT_FOUND"}
|
| $ curl -i -b cookies.txt -H "Content-Type: application/json" -d "{pas du json" http://localhost:8080/api/rendez-vous
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 45
{"statusCode":400,"cle":"ERRORS.BAD_REQUEST"}
|
Nous connaissons maintenant tout ce qu'il faut savoir du serveur pour écrire son client.