cet article.
A noter que toute les méthodes safe exigent d'être idempotent mais que l'inverse n'est pas forcément vrai. Pour ce qui est des trois méthodes étudiées plus bas : POST, PUT et PATCH, aucune n'est safe puisqu'elle provoquent une mise à jour, donc une modification de l'état de la ressource.
La méthode POST est un peu notre matière première. Elle est définie par la RFC7231 qui nous dit ceci :
The POST method requests that the target resource process the representation enclosed in the request according to the resource's own specific semantic
Résumé simplement : POST permet de faire tout et n'importe quoi tant que ça concerne la représentation de la ressource envoyée dans la requête. On peut donc bien faire de la mise à jour via POST. Le veut-on cependant ? Non, pas vraiment. Deux méthodes dédiées (PUT et PATCH) existent pour ce besoin spécifique, autant les utiliser.
Il va sans dire qu'une telle méthode n'est ni safe ni idempotent.
La méthode PUT (définie par la RFC7231) exige d'être idempotent mais n'est pas safe. Cela signifie que l'appeler à plusieurs reprises sur une URI donnée ne devra induire qu'un seul et unique effet de bord côté serveur, c'est-à-dire la création ou la mise à jour d'une donnée identifiée par le client.
En outre, utiliser PUT indique que la ressource manipulée est :
Exemple :
PUT /products/my-octo-t-shirt HTTP/1.1
Content-Type: application/json
{ "name": "my-octo-t-shirt", "price": 15.0, "weight": 0.3 }
Si la ressource "my-octo-t-shirt" n'existe pas alors elle sera créée (201 - CREATED).
Si la ressource "my-octo-t-shirt" existe déjà alors elle sera mise à jour (200 - OK ou 204 - NO CONTENT). Rien n'indique dans la spécification ce que doit contenir la réponse. Il est cependant d'usage de renvoyer une représentation de la ressource mise à jour.
La méthode PATCH (définie par la RFC5789) ne fait pas partie des méthodes initialement proposées dans la RFC7230. Elle a été ajoutée au standard HTTP en 2010 afin de remplacer POST quand il s'agit d'effectuer des mises à jour partielles.
La méthode PATCH n'est pas safe et n'exige pas l'idempotence (comme le montre cet exemple), c'est-à-dire qu'elle peut induire des effets de bord (logique puisqu'elle modifie la ressource côté serveur) et qu'elle ne garantit pas qu'après N appels successifs sur la même URI avec le même body, le serveur n'ait pas abouti successivement à N représentations différentes de la ressource identifiée par cette URI.
Il est possible de concevoir une implémentation de PATCH qui soit idempotent, néanmoins cela implique que ce cette garantie sera assurée par la couche applicative et non protocole.
De plus, la caractéristique partielle de la mise à jour effectuée par PATCH implique que celle-ci s'applique à un état connu de la ressource, rendant nécessaire l'introduction d'un mécanisme permettant de distinguer un état d'un autre (l'etag par exemple).
L'une des autres difficultés dans l'utilisation de PATCH réside dans la manière de décrire les modifications à apporter à la ressource identifiée par l'URI. Ainsi, la RFC définissant PATCH ne dit pas comment décrire un changement de l'état de la ressource. Dommage !
Exemple :
PATCH /products/my-octo-t-shirt HTTP/1.1
Content-Type: "A choisir en fonction du format du body"
"Description des changements dans un format non-standardisé par la RFC"
La communauté a décidé de combler ce manque en proposant différents formats permettant de décrire ces changements : JSON Patch, JSON-LD, JSON Merge Patch, etc. On utilisera JSON Merge Patch dans la suite de cet article qui, bien que proposant moins de fonctionnalités que JSON Patch, offre l'avantage d'être une implémentation très simple à manipuler.
Commençons par imaginer un profil d'utilisateur qui ressemblerait à ça :
{
"id": 1,
"first_name" : "Tom",
"last_name": "Smith",
"email": "tom.smith@example.com"
"phone": {
"home": "0123456789",
"mobile": "9876543210"
},
"address": {
"street": "34 avenue de l'opera",
"zip_code": "75002",
"city": "PARIS"
}
}
Considérons le cas où l'on souhaite mettre à jour l'adresse.
PUT /users/1 HTTP/1.1
Content-Type: application/json
{
"id": 1,
"first_name" : "Tom",
"last_name": "Smith",
"email": "tom.smith@example.com"
"phone": {
"home": "0123456789",
"mobile": "9876543210"
},
"address": {
"street": "50 avenue des Champs Elysées",
"zip_code": "75008",
"city": "PARIS"
}
}
PATCH /users/1
Content-Type: application/merge-patch+json
{
"address": {
"street": "50 avenue des Champs Elysées",
"zip_code": "75008"
}
}
PUT /users/1/address HTTP/1.1
Content-Type: application/json
{
"street": "50 avenue des Champs Elysées",
"zip_code": "75008",
"city": "PARIS"
}
Holà, holà, j'entends déjà les huées dans le public. Oui nous avons choisi d'introduire une nouvelle sous-ressource ("/users/:id/address") et ce pour deux raisons :
Analysons un peu les bénéfices des approches proposées :
Idempotent (d'après la RFC) | Taille payload | Format des instructions de modification | Bilan | |
---|---|---|---|---|
1. PUT | Oui | Volumineux | Standard | ■■■ |
2. PATCH | Non | Léger | Custom | ■■■ |
3. PUT /address | Oui | Léger | Standard | ■■■ |
Imaginons la ressource "facture" suivante, disponible à l'URI /bills/1 :
{ "amount": 25.0, "payment_date": "2018-01-01", "status": "pending" }
Comment modifier le statut de cette facture (de pending à paid) afin d'indiquer que le paiement a été effectué ?
PUT /bills/1 HTTP/1.1
Content-Type: application/json
{ "amount": 25.0, "payment_date": "2018-01-01", "status": "paid" }
PATCH /bills/1 HTTP/1.1
Content-Type: application/merge-patch+json
{ "status": "paid" }
PUT /bills/1/status HTTP/1.1
Content-Type: application/json
"paid"
PUT /bills/1/payment HTTP/1.1
Content-Type: application/json
{ "iban": "FR3330002005500000157841Z25", ...otherPaymentInformation }
Qui aboutirait à la représentation suivante de la ressource /bills/1 :
GET /bills/1 HTTP/1.1
{ "amount": 25.0,"payment_date": "01-01-2018", "status": "paid" }
Attention, on connaît ici la localisation de la ressource de paiement, c'est-à-dire son URI, raison pour laquelle on peut utiliser PUT. A ne pas confondre avec /payments qui indique l'existence d'une collection de paiements et supposerait l'utilisation de POST plutôt que PUT.
Idempotent (d'après la RFC) | Taille payload | Format des instructions de modification | Bilan | |
---|---|---|---|---|
1. PUT | Oui | Volumineux | Standard | ■■■ |
2. PATCH | Non | Léger | Custom | ■■■ |
3. PUT /status | Oui | Léger | Standard | ■■■ |
4. PUT /payment | Oui | Moyen* | Standard | ■■■ |
* A pondérer car la création de cette ressource permet au SI d'identifier de nouvelles informations (RIB, adresse de facturation, etc.)
Pour les trois premiers cas, la conclusion de l'analyse est la même que dans la section précédente : on préfèrera modifier une sous-ressource avec PUT (3) car :
En revanche, quand il s'agit de considérer la particularité de cette étude de cas, à savoir la mise à jour d'un statut, on préfèrera largement la solution 4. En effet, si le statut du paiement est effectivement un concept métier, il est largement préférable que ce dernier soit garanti par le serveur et non le client, donc fermé à la modification par ce dernier. La mise à jour de ce statut est ce qui fait la valeur ajoutée de notre API, et ce quelque soit l'analyse effectuée par le client.
A noter qu'il est parfaitement possible de concevoir une utilisation de PATCH qui aboutisse toujours au même résultat, donc qui soit fonctionnellement idempotent. Néanmoins, plutôt que de laisser flotter une ambiguïté, on préfèrera toujours donner un maximum d'information au consommateur de l'API en lui garantissant que l'action n'aboutira pas à deux résultats distincts par l'utilisation d'un verbe exigeant d'être idempotent : PUT.
Plus généralement, le style d'architecture REST suggère d'identifier les ressources manipulables par les clients. Ces ressources peuvent aussi bien être des entités présentes en base de données comme un produit, un utilisateur ou un article mais aussi des processus métier que le client manipule au travers d'API REST et qui sont l'abstraction de complexités inhérentes aux SI manipulés.
Imaginons la ressource "article" suivante, disponible à l'URI /articles/1 :
{ "title": "PUT vs PATCH", "tags": ["http", "api"] }
Comment modifier la liste des tags de cet article ? Comment lui ajouter des éléments ?
PUT /articles/1 HTTP/1.1
Content-Type: application/json
{ "title": "PUT vs PATCH","tags": ["http", "api", "rest", "put", "patch"] }
PATCH /articles/1 HTTP/1.1
Content-Type: application/merge-patch+json
{ "tags": ["http", "api", "rest", "put", "patch"] }
PUT /articles/1/tags HTTP/1.1
Content-Type: application/json
["http", "api", "rest", "put", "patch"]
PUT /articles/1/tags/put
PUT /articles/1/tags/patch
Idempotent (d'après la RFC) | Taille payload | Format des instructions de modification | Bilan | |
---|---|---|---|---|
1. PUT | Oui | Volumineux | Standard | ■■■ |
2. PATCH | Non | Léger | Custom | ■■■ |
3. PUT /tags | Oui | Moyen | Standard | ■■■ |
4. PUT /tags/:item | Oui | Léger* | Standard | ■■■ |
* A pondérer car le nombre d'appels augmente.
Encore une fois, les trois premières approches aboutissent à la même conclusion que dans les deux sections précédentes : on préfèrera modifier une sous-ressource avec PUT (3) car :
Néanmoins, si nous devions opter pour une solution quand il s'agit de mettre à jour les tags d'un article, nous opterions pour la solution 4. En effet, celle-ci offre l'élégance de se passer de payload, donc de ne travailler que sur l'URI. Cette possibilité est loin d'être anodine puisqu'elle offre "gratuitement" la possibilité de supprimer un tag de la liste par l'utilisation de la méthode DELETE sur cette même URI (exemple: "DELETE /articles/1/tags/put"). L'approche (3) reste cependant tout à fait valide si l'objectif n'est pas seulement de manipuler les tags.
Prenons le cas de la ressource "devis", disponible à l'url "/quotes/:id" :
{
"id": "azerty1234",
"billable": true,
"payment_mean": "money_transfer",
"payment_deadline": "01-01-2018",
"payment_status": "pending"
"cost_estimation": 187.0,
"fees": 25.0,
"part_id": "foobar1234",
"reduced_tax": true,
"warranty": true,
"warranty_type": "A",
"warranty_duration": "18 months"
}
Cette ressource est volontairement plus confuse que les exemples proposés précédemment. Pourquoi ? Car elle reflète de nombreuses API legacy que nous rencontrons chez nos clients et qui ne sont pas conçues en mode API-first mais ont été créées de manière ad-hoc pour répondre à un besoin spécifique en provenance d'un partenaire ou d'une volonté d'ouverture du SI.
Essayons de mettre à jour la propriété billable :
PUT /quotes/azerty1234 HTTP/1.1
Content-Type: application/json
{
"id": "azerty1234",
"billable": false,
"payment_mean": "money_transfer",
"payment_deadline": "01-01-2018",
"payment_status": "pending"
"cost_estimation": 187.0,
"fees": 25.0,
"part_id": "foobar1234",
"reduced_tax": true,
"warranty": true,
"warranty_type": "A",
"warranty_duration", "18 months"
}
PATCH /quotes/azerty1234 HTTP/1.1
Content-Type: application/merge-patch+json
{ "billable": false }
PUT /quotes/azerty1234/billable HTTP/1.1
Content-Type: application/json
false
Idempotent (d'après la RFC) | Taille payload | Format des instructions de modification | Bilan | |
---|---|---|---|---|
1. PUT | Oui | Volumineux | Standard | ■■■ |
2. PATCH | Non | Léger | Custom | ■■■ |
3. PUT /billable | Oui | Léger | Standard | ■■■ |
Le bilan présenté ci-dessus nous indique qu'il faut préférer l'identification d'une sous-ressource en complément du verbe PUT (3) afin de mettre à jour le champ "billable". Sauf qu'une telle approche n'est pas viable car elle implique qu'il faut créer autant de sous-ressources que d'attributs à modifier, ce qui n'a pas de sens : une nouvelle API utilisant PUT pour /quotes/:id/payment_mean, une autre pour /quotes/:id/payment_deadline, etc.
Dans ce cas, on devrait préférer la méthode PATCH, n'est-ce pas ?
Oui… Et non. Je m'explique :
Alors, je PUT ou je PATCH ?
La règle générale est de commencer par étudier le métier qu'on manipule et de se poser la question : mon métier exige-t-il l'idempotence ? Mes modifications peuvent-elle être apportées de sorte que si je les applique de nouveau, j'aboutisse au même résultat ? Si la réponse à cette question est "oui" alors pas de doute, utilisez PUT.
La place de PATCH dans cette équation est plus délicate. Quand il s'agit de mise à jour partielle, on préférera effectuer des PUT sur des sous-ressources de nos entités. Cela permet d'une part d'identifier ce qui est manipulable de ce qui ne l'est pas et, d'autre part, de garantir le caractère idempotent de l'opération réalisée. Néanmoins cela n'est pas gratuit et demande un travail afin d'identifier ces sous-ressources.
Nous recommandons de réserver PATCH aux rares cas où le client désire mettre à jour plusieurs champs d'une même sous-ressource, où cette ressource est cohérente d'un point de vue métier, où ce dernier accepte d'intégrer un format custom afin de décrire les modifications qu'il souhaite apporter et où la quantité de champs modifiables est trop grande pour justifier l'utilisation de PUT (oui, tout ça). On perd au passage le caractère idempotent natif de PUT (même s'il est possible dans la pratique d'effectuer des PATCH idempotent, cela n'est pas garanti par la couche protocole).
Certains, comme Roy Fielding par exemple, diront qu'effectuer une mise à jour partielle n'est pas dans l'esprit de REST puisqu'on ne manipule pas une représentation de la ressource mais plutôt un ensemble d'instructions visant à modifier cette dernière...
Il va sans dire que l'utilisation de PUT ou PATCH ne résout pas tous les problèmes. Des stratégies comme Compare And Set peuvent être nécessaires afin de ne pas aboutir à des représentations corrompues de nos ressources, et requiert donc l'introduction de mécanismes additionnels afin d'identifier la représentation d'une ressource sur laquelle s'applique une requête PATCH (avec un etag par exemple). En outre, l'approche que nous suggérons dans cet article, à savoir de travailler sur des sous-ressources, n'est pas applicable à tous les cas d'usage et il convient de bien réfléchir sa conception afin d'identifier la pertinence de cette démarche dans un contexte différent.
Globalement, concevoir une API REST peut paraître simple : création avec POST, mise à jour avec PUT, suppression avec DELETE et lecture avec GET. Cependant, afin que votre API apporte de la valeur à votre SI, de sorte que ce qui est perçu comme un centre de coût devienne un centre de valeurs, une API REST doit être plus qu'un simple CRUD sur des ressources. Elle doit exprimer le métier de votre entreprise donc permettre la manipulation de vos entités autant que de vos workflows, exprimés sous forme de ressources nommés et manipulables.
Et toi, t'es idempotent ?