Should I PUT or should I PATCH?

Le style d'architecture REST proposé par Roy Fielding au début des années 2000 suggère de ne pas inventer de nouveaux protocoles afin d'exprimer les opérations appliquées aux ressources de nos systèmes. Au contraire, il propose de s'appuyer sur le protocole HTTP, protocole définissant un ensemble restreint de méthodes (ou verbes) permettant d'exprimer différentes manières d'interagir avec nos ressources.

Une des opérations classiques de tout système est la modification de l'état d'une ressource. Et dans HTTP, cette mise à jour peut s'effectuer via trois méthodes distinctes : POST, PUT et PATCH.

Cela implique donc de lever une ambiguïté : quelle méthode utiliser ?

Pour répondre à cette question, je vous propose d'étudier 4 scénarios discutant les intérêts de ces méthodes :

  • la mise à jour d'un profil utilisateur ;
  • la mise à jour d'un statut ;
  • la mise à jour d'une liste d'éléments ;
  • la modification d'une API aux propriétés hétérogènes (fréquentes dans un contexte "legacy").

Mais avant tout quelques rappels...

Idempotence et safeness

Les méthodes HTTP sont caractérisées selon qu'elles respectent ou non deux garanties :

  • safeness : toute méthode safe garantit qu'elle n'altère pas l'état de la ressource côté serveur ;
  • idempotence : toute méthode idempotent garantit qu'elle aboutira toujours au même résultat. C'est une spécificité particulièrement intéressante dans un contexte de mobilité (entre autres), comme discuté dans 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

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

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 :

  • identifiée par l'URI de la requête ;
  • définie en totalité par le corps de la requête ;
  • mise à jour si cette ressource existe déjà ;
  • créée si la ressource n'existe pas.

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

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.

Etude de cas n°1 : la mise à jour d'un profil utilisateur

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.

1. Mise à jour avec PUT :

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"
	}
}

2. Mise à jour avec PATCH (via JSON Merge Patch) :

PATCH /users/1
Content-Type: application/merge-patch+json

{ 
	"address": { 
		"street": "50 avenue des Champs Elysées",
		"zip_code": "75008"
	}
}

3. Mise à jour avec PUT sur une sous-ressource de /users :

PUT /users/1/address HTTP/1.1
Content-Type: application/json

{
	"street": "50 avenue des Champs Elysées",
	"zip_code": "75008",
	"city": "PARIS"
}

Bilan

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 :

  1. Cela nous permet d'utiliser PUT (donc de rester idempotent), et de ne pas introduire dans notre système un format non-standardisé (JSON Merge Patch en l'occurrence) ;
  2. Cela nous permet de clarifier ce que le système permet de mettre à jour de ce qui ne le permet pas. En effet, on pourrait croire avec PATCH que le champ "id" peut être mis à jour car présent dans le payload initial. Or ce n'est pas le cas.

Analysons un peu les bénéfices des approches proposées :

Idempotent (d'après la RFC)Taille payloadFormat des instructions de modificationBilan
1. PUTOuiVolumineuxStandard■■■
2. PATCHNonLégerCustom■■■
3. PUT /addressOuiLégerStandard■■■

Etude de cas n°2 : la mise à jour d'un statut

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é ?

1. Mise à jour avec PUT :

PUT /bills/1 HTTP/1.1
Content-Type: application/json

{ "amount": 25.0, "payment_date": "2018-01-01", "status": "paid" }

2. Mise à jour avec PATCH (via JSON Merge Patch) :

PATCH /bills/1 HTTP/1.1
Content-Type: application/merge-patch+json

{ "status": "paid" }

3. PUT sur le statut de la ressource /bills/1 :

PUT /bills/1/status HTTP/1.1
Content-Type: application/json

"paid"

4. PUT sur une sous-ressource unique et "métier" de la ressource /bills/1 :

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.

Bilan

Idempotent (d'après la RFC)Taille payloadFormat des instructions de modificationBilan
1. PUTOuiVolumineuxStandard■■■
2. PATCHNonLégerCustom■■■
3. PUT /statusOuiLégerStandard■■■
4. PUT /paymentOuiMoyen*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 :

  • Elle exige d'être idempotent
  • Travailler sur une sous-ressource reflète mieux la réalité de ce que permet le SI
  • Elle offre un équilibre volume de données/contraintes techniques satisfaisant (pas de format custom pour exprimer les modifications à apporter).

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.

Etude de cas n°3 : la mise à jour d'une liste

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 ?

1. Mise à jour avec PUT :

PUT /articles/1 HTTP/1.1
Content-Type: application/json

{ "title": "PUT vs PATCH","tags": ["http", "api", "rest", "put", "patch"] }

2. Mise à jour avec PATCH (via JSON Patch Merge) :

PATCH /articles/1 HTTP/1.1
Content-Type: application/merge-patch+json

{ "tags": ["http", "api", "rest", "put", "patch"] }

3. Mise à jour avec PUT sur une sous-ressource de /articles/1

PUT /articles/1/tags HTTP/1.1
Content-Type: application/json

["http", "api", "rest", "put", "patch"]

4. Mise à jour avec PUT sur un item de tags :

PUT /articles/1/tags/put
PUT /articles/1/tags/patch

Bilan

Idempotent (d'après la RFC)Taille payloadFormat des instructions de modificationBilan
1. PUTOuiVolumineuxStandard■■■
2. PATCHNonLégerCustom■■■
3. PUT /tagsOuiMoyenStandard■■■
4. PUT /tags/:itemOuiLé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 :

  • Elle est idempotent
  • Travailler sur une sous-ressource reflète mieux la réalité de ce que permet le SI
  • Elle offre un équilibre volume de données/contraintes techniques satisfaisant (pas de format custom pour exprimer les modifications à apporter).

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.

Etude de cas n°4 : modification d'un devis

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 :

1. Mise à jour avec PUT :

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"
}

2. Mise à jour avec PATCH (via JSON Patch Merge) :

PATCH /quotes/azerty1234 HTTP/1.1
Content-Type: application/merge-patch+json

{ "billable": false }

3. Mise à jour avec PUT sur une sous-ressource de /articles/1

PUT /quotes/azerty1234/billable HTTP/1.1
Content-Type: application/json

false

Bilan

Idempotent (d'après la RFC)Taille payloadFormat des instructions de modificationBilan
1. PUTOuiVolumineuxStandard■■■
2. PATCHNonLégerCustom■■■
3. PUT /billableOuiLégerStandard■■■

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 :

  • Non car avoir besoin de modifier les N attributs d'une ressource est finalement assez rare, une fois exclus les 3 cas étudiés précédemment ;
  • Non à nouveau quand on considère l'étude de cas juste au-dessus où la ressource présentée n'en est pas réellement une. Cette ressource est une agrégation de données vaguement liées entre elles mais qui ne possèdent pas une cohérence métier. Elle est probablement cohérente pour l'écran du client qui la consomme, mais rappelons qu'une API ne doit pas être conçue pour un écran mais bien pour un besoin.
  • Oui car on peut répondre au besoin immédiat et proposer la mise à jour partielle de cette ressource via PATCH. Néanmoins, c'est un "API smell", indiquant un défaut de conception (une ressource avec trop d'informations) plutôt qu'une bonne utilisation du style d'architecture REST.

Synthèse

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 ?