{"openapi":"3.1.0","info":{"title":"Simple CRUD","description":"Une API REST de gestion d'utilisateurs, en Java avec Spring Boot.\n\n- **Erreurs** : format Problem Details (RFC 9457), avec un `code` stable et, pour les données refusées, la liste des champs en cause.\n- **Concurrence** : chaque lecture renvoie un `ETag` ; envoyé dans `If-Match`, il empêche d'écraser une modification faite entre-temps (412).\n- **Modification partielle** : `PATCH` au format JSON Merge Patch (RFC 7396).\n- **API externe** : la commune est vérifiée auprès de geo.api.gouv.fr.\n- **Démo publique** : 300 requêtes par minute et par client, dont 30 écritures (au-delà : 429) ; les données sont rétablies chaque nuit.","version":"v1"},"servers":[{"url":"https://simple-crud.2-28-103-231.sslip.io","description":"Generated server url"}],"tags":[{"name":"Utilisateurs","description":"Créer, lire, modifier et supprimer des utilisateurs"},{"name":"Communes","description":"Les communes d'un code postal, d'après l'API Géo de l'État"}],"paths":{"/api/v1/users/{id}":{"get":{"tags":["Utilisateurs"],"summary":"Lire un utilisateur","description":"Avec If-None-Match, répond 304 sans corps si la version n'a pas changé.","operationId":"get","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"If-None-Match","in":"header","description":"Un ETag déjà lu, par exemple \"3\"","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"L'utilisateur","headers":{"ETag":{"description":"La version de l'utilisateur, à renvoyer dans If-Match","style":"simple"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"304":{"description":"Inchangé depuis la version indiquée dans If-None-Match"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"put":{"tags":["Utilisateurs"],"summary":"Remplacer un utilisateur","description":"Remplacement complet : un champ facultatif absent est effacé. Pour ne changer qu'un champ, utilisez PATCH.","operationId":"replace","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"If-Match","in":"header","description":"Facultatif. L'ETag lu auparavant : la modification n'a lieu que si l'utilisateur n'a pas changé depuis (sinon 412).","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRequest"}}},"required":true},"responses":{"200":{"description":"Remplacé","headers":{"ETag":{"description":"La version de l'utilisateur, à renvoyer dans If-Match","style":"simple"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"412":{"$ref":"#/components/responses/PreconditionFailed"},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"422":{"$ref":"#/components/responses/UnprocessableContent"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"},"429":{"$ref":"#/components/responses/TooManyRequests"},"413":{"$ref":"#/components/responses/ContentTooLarge"}}},"delete":{"tags":["Utilisateurs"],"summary":"Supprimer un utilisateur","operationId":"delete","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"If-Match","in":"header","description":"Facultatif. L'ETag lu auparavant : la modification n'a lieu que si l'utilisateur n'a pas changé depuis (sinon 412).","required":false,"schema":{"type":"string"}}],"responses":{"204":{"description":"Supprimé"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"412":{"$ref":"#/components/responses/PreconditionFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"patch":{"tags":["Utilisateurs"],"summary":"Modifier une partie d'un utilisateur","description":"JSON Merge Patch (RFC 7396) : seuls les champs envoyés changent, null efface un champ.","operationId":"patch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"If-Match","in":"header","description":"Facultatif. L'ETag lu auparavant : la modification n'a lieu que si l'utilisateur n'a pas changé depuis (sinon 412).","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/merge-patch+json":{"schema":{"type":"object"},"examples":{"Changer l'âge":{"description":"Changer l'âge","value":{"age":37}},"Effacer l'âge":{"description":"Effacer l'âge","value":{"age":null}},"Déménager":{"description":"Déménager","value":{"postalCode":"33000","city":null}}}}},"required":true},"responses":{"200":{"description":"Modifié","headers":{"ETag":{"description":"La version de l'utilisateur, à renvoyer dans If-Match","style":"simple"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"412":{"$ref":"#/components/responses/PreconditionFailed"},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"422":{"$ref":"#/components/responses/UnprocessableContent"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"},"429":{"$ref":"#/components/responses/TooManyRequests"},"413":{"$ref":"#/components/responses/ContentTooLarge"}}}},"/api/v1/users":{"get":{"tags":["Utilisateurs"],"summary":"Lister les utilisateurs","description":"Paginé (20 par page, 100 au plus), trié par date de création décroissante par défaut.","operationId":"list","parameters":[{"name":"q","in":"query","description":"Texte cherché dans le nom ou l'e-mail, sans tenir compte de la casse","required":false,"schema":{"type":"string","maxLength":100,"minLength":0},"example":"ada"},{"name":"page","in":"query","description":"Zero-based page index (0..N)","required":false,"schema":{"type":"integer","default":0,"minimum":0}},{"name":"size","in":"query","description":"The size of the page to be returned","required":false,"schema":{"type":"integer","default":20,"minimum":1}},{"name":"sort","in":"query","description":"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.","required":false,"schema":{"type":"array","default":["createdAt,DESC"],"items":{"type":"string"}}}],"responses":{"200":{"description":"Une page d'utilisateurs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PagedModelUser"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"tags":["Utilisateurs"],"summary":"Créer un utilisateur","description":"Si un code postal est donné, la commune est vérifiée auprès de l'API Géo.","operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRequest"}}},"required":true},"responses":{"201":{"description":"Créé","headers":{"ETag":{"description":"La version de l'utilisateur, à renvoyer dans If-Match","style":"simple"},"Location":{"description":"L'adresse du nouvel utilisateur","style":"simple"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"422":{"$ref":"#/components/responses/UnprocessableContent"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"},"429":{"$ref":"#/components/responses/TooManyRequests"},"413":{"$ref":"#/components/responses/ContentTooLarge"}}}},"/api/v1/communes":{"get":{"tags":["Communes"],"summary":"Communes d'un code postal","description":"Une liste vide si le code postal n'existe pas. Réponse publique, à garder en cache une heure.","operationId":"byPostalCode","parameters":[{"name":"postalCode","in":"query","description":"Code postal, 5 chiffres","required":true,"schema":{"type":"string","pattern":"\\d{5}"},"example":69001}],"responses":{"200":{"description":"Les communes, par ordre alphabétique","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Commune"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}}},"components":{"schemas":{"Problem":{"type":"object","description":"Une erreur, au format Problem Details (RFC 9457)","properties":{"type":{"type":"string","description":"Absent : « about:blank », le statut HTTP suffit à qualifier l'erreur"},"title":{"type":"string","description":"Le libellé du statut HTTP"},"status":{"type":"integer","format":"int32"},"detail":{"type":"string","description":"L'explication, à afficher"},"instance":{"type":"string","description":"L'adresse appelée"},"code":{"type":"string","description":"Code stable, à tester dans un programme"},"errors":{"type":"array","description":"Les champs en cause, quand il y en a","items":{"$ref":"#/components/schemas/FieldViolation"}}}},"FieldViolation":{"type":"object","description":"Un champ refusé","properties":{"field":{"type":"string","example":"email"},"message":{"type":"string","example":"L'e-mail n'est pas valide."}}},"UserRequest":{"type":"object","description":"Les données d'un utilisateur, à la création comme au remplacement","properties":{"name":{"type":"string","description":"Nom complet","example":"Ada Lovelace","maxLength":100,"minLength":0},"email":{"type":"string","format":"email","description":"Adresse e-mail, unique (sans tenir compte de la casse)","example":"ada@example.org","maxLength":254,"minLength":0},"age":{"type":"integer","format":"int32","description":"Âge, facultatif","example":36,"maximum":120,"minimum":1},"postalCode":{"type":"string","description":"Code postal français, facultatif","example":"69001","pattern":"\\d{5}"},"city":{"type":"string","description":"Commune. Facultative quand le code postal ne dessert qu'une commune : le serveur la complète. Le nom officiel est enregistré.","example":"Lyon","maxLength":100,"minLength":0}},"required":["email","name"]},"User":{"type":"object","description":"Un utilisateur","properties":{"id":{"type":"integer","format":"int64","example":42},"name":{"type":"string","example":"Ada Lovelace"},"email":{"type":"string","example":"ada@example.org"},"age":{"type":"integer","format":"int32","example":36},"postalCode":{"type":"string","example":"69001"},"city":{"type":"string","example":"Lyon"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PageMetadata":{"type":"object","properties":{"size":{"type":"integer","format":"int64"},"number":{"type":"integer","format":"int64"},"totalElements":{"type":"integer","format":"int64"},"totalPages":{"type":"integer","format":"int64"}}},"PagedModelUser":{"type":"object","properties":{"content":{"type":"array","items":{"$ref":"#/components/schemas/User"}},"page":{"$ref":"#/components/schemas/PageMetadata"}}},"Commune":{"type":"object","description":"Une commune française","properties":{"code":{"type":"string","description":"Code INSEE","example":"69123"},"name":{"type":"string","description":"Nom officiel","example":"Lyon"},"departmentCode":{"type":"string","description":"Code du département","example":"69"}}}},"responses":{"BadRequest":{"description":"Requête illisible ou paramètre invalide","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"examples":{"MALFORMED_REQUEST":{"value":{"title":"Bad Request","status":400,"detail":"Champ inconnu : « nom ». Champs acceptés : age, city, email, name, postalCode.","instance":"/api/v1/users","code":"MALFORMED_REQUEST"}}}}}},"NotFound":{"description":"Utilisateur inconnu","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"examples":{"USER_NOT_FOUND":{"value":{"title":"Not Found","status":404,"detail":"Aucun utilisateur n'a l'identifiant 42.","instance":"/api/v1/users","code":"USER_NOT_FOUND"}}}}}},"Conflict":{"description":"E-mail déjà utilisé, ou modification concurrente","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"examples":{"EMAIL_ALREADY_USED":{"value":{"title":"Conflict","status":409,"detail":"Cet e-mail est déjà utilisé par un autre utilisateur.","instance":"/api/v1/users","code":"EMAIL_ALREADY_USED","errors":[{"message":"Cet e-mail est déjà utilisé par un autre utilisateur.","field":"email"}]}}}}}},"PreconditionFailed":{"description":"L'ETag d'If-Match n'est plus le bon : relire, puis réessayer","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"examples":{"PRECONDITION_FAILED":{"value":{"title":"Precondition Failed","status":412,"detail":"La ressource a été modifiée depuis votre lecture : relisez-la avant de la modifier.","instance":"/api/v1/users","code":"PRECONDITION_FAILED"}}}}}},"UnsupportedMediaType":{"description":"Content-Type refusé","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"examples":{"UNSUPPORTED_MEDIA_TYPE":{"value":{"title":"Unsupported Media Type","status":415,"detail":"Le type de contenu text/plain n'est pas accepté ici. Types acceptés : [application/json].","instance":"/api/v1/users","code":"UNSUPPORTED_MEDIA_TYPE"}}}}}},"UnprocessableContent":{"description":"Données refusées, champ par champ","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"examples":{"VALIDATION_FAILED":{"value":{"title":"Unprocessable Content","status":422,"detail":"Certaines données sont invalides.","instance":"/api/v1/users","code":"VALIDATION_FAILED","errors":[{"message":"L'e-mail n'est pas valide.","field":"email"}]}}}}}},"BadGateway":{"description":"L'API Géo a renvoyé une erreur","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"examples":{"GEO_UNAVAILABLE":{"value":{"title":"Bad Gateway","status":502,"detail":"Le service des communes (geo.api.gouv.fr) a renvoyé une erreur. Réessayez dans un instant.","instance":"/api/v1/users","code":"GEO_UNAVAILABLE"}}}}}},"GatewayTimeout":{"description":"L'API Géo n'a pas répondu à temps","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"examples":{"GEO_TIMEOUT":{"value":{"title":"Gateway Timeout","status":504,"detail":"Le service des communes (geo.api.gouv.fr) n'a pas répondu à temps. Réessayez dans un instant.","instance":"/api/v1/users","code":"GEO_TIMEOUT"}}}}}},"ContentTooLarge":{"description":"Corps de requête au-delà de 16 Ko","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"examples":{"CONTENT_TOO_LARGE":{"value":{"title":"Content Too Large","status":413,"detail":"La requête dépasse 16 Ko.","instance":"/api/v1/users","code":"CONTENT_TOO_LARGE"}}}}}},"TooManyRequests":{"description":"Trop de requêtes pour ce client : attendre Retry-After secondes","headers":{"Retry-After":{"description":"Le nombre de secondes à attendre","schema":{"type":"integer","format":"int32"}}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"examples":{"RATE_LIMITED":{"value":{"title":"Too Many Requests","status":429,"detail":"Trop de requêtes en peu de temps : réessayez dans 12 s.","instance":"/api/v1/users","code":"RATE_LIMITED"}}}}}}}}}