Aller au contenu principal

API HTTP : verbes et endpoints

Cette session documente le bon usage des verbes HTTP et la manière conseillée d'écrire ses endpoints.

Anatomie d'une requête http

Une requête HTTP est composée de plusieurs éléments.

ElementExempleUtilisation
VerbePOST, GET, PUTAction effectuée sur le endpoint
Hosthttps://gamecodeclub.comHôte où l'API est présentée
Endpointapi/v1/gamesEndpoint qui est interrogé par l'API
Endpoint?key1=valueA&key2=value2Paramètres optionnels de la requête
HeadersAuthorization: Basic <credentials>Headers donnant du contexte à la demande
PayloadObjet Game { title = "MyGreatGame", type = "puzzle" }Contenu pour ajouter/éditer

Nous allons détailler les verbes et endpoints ci-dessous.

Notez que les endpoints dépendent des contrats de l'API, mais le hôte peut être différent et il est généralement séparé des endpoints. C'est une donnée généralement configurable, notamment pour pouvoir changer l'API cible selon l'environnement.

Les headers ajoutent du contexte, tels que des précisions sur la demande (de format, de langue...), mais également, généralement, l'authentication nécessaire si l'API cible limite son usage selon l'utilisateur.

Les verbes : correspondance CRUD et Post/Get/Put/Delete

Les verbes HTTP vous permettent d'indiquer l'action que vous voulez faire sur un endpoint donné.

Quand un contrat contient les endpoints d'API, il contient généralement les verbes autorisés pour chacun des endpoints.

Les opération de bases sont les opérations CRUD : Create, Read, Update, Delete. Voici les verbes équivalents:

Verbe HTTPCRUDUtilisation
POSTC (Create)Créer une nouvelle ressource
GETR (Read)Lire une ou plusieurs resource(s)
PUTU (Update)Mettre à jour une resource existante (remplacement complet)
PATCHU (Update)Modifier partiellement une resource existante
DELETED (Delete)Effacer une resource existante

Il existe d'autres verbes qui ont différentes utilisations :

  • OPTIONS : décrit les options de communication.
  • HEAD : identique à GET, mais sans le corps de la réponse.
  • QUERY : un verbe ajouté relativement récemment, qui permet d'envoyer directement une requête au serveur cible.

Le verbe GET - Récupérer

GET est un des plus vieux verbes HTTP existant, c'est celui qui sert notamment à charger une page web, et le verbe par défaut pour des outils comme curl.

Dans le cadre d'une API, GET va permettre de récupérer une ou plusieurs resource.

Un GET sur api/v1/games est un "GetAll()", il permet d'obtenir TOUTES les ressources disponibles à cet endpoint.

La réponse contient une liste avec les éléments demandés

Un GET sur api/v1/games/{:id} est un "Get(id)", il permet d'obtenir uniquement un élément, avec l'ID correspondant.

info

Le pluriel à /games/{:id} pour récupérer un seul élément vous surprend ? Rendez-vous dans la section suivante pour comprendre cette utilisation.

Le verbe POST - Ajouter un élément

Le verbe POST existe aussi depuis longtemps, il était notoire dans les années 1990-2000 pour l'envoi de formulaires.

Envoyer un POST sur api/v1/games, avec un objet défini dans la Payload de la requête, ça permet de créer un élément. L'API doit confirmer en retour que l'élément est bien créé.

Les verbes PUT et PATCH - Mise à jour

L'opération d'Update peut se faire par deux verbes : PUT et PATCH.

La différence entre les deux est que PATCH est pour une mise à jour partielle, tandis que PUT doit pouvoir recevoir l'intégralité de l'objet à mettre à jour. Couramment, on voit principalement le PUT.

Le verbe PATCH peut être utile dans de nombreux cas, mais nécessite généralement de bien penser son API pour les cas de remplacement. PUT est le plus fréquent, mais généralement suite à un GET où on a récupéré tout l'objet.

Le verbe DELETE - Supprimer un élément

Comme son nom l'indique, l'envoi du verbe Delete api/v1/games/{:id} permet de supprimer l'élément ayant cet ID s'il existe. Pas besoin d'envoyer une payload.

Comme c'est une action destructive, il faut bien sûr cadrer son utilisation : ne pas rendre tout supprimable aussi facilement (notamment s'il y a des interdépendances entre les objets), voire ne pas faire de suppression directe dans la base de données (plutôt inverser un booléen et "masquer" l'élément).

Du bon usage des endpoints

La structure générale d'un endpoint d'API est comme suit. Comme dit plus haut, la partie "host" est liée à l'environnement, les contrats d'API ne doivent idéalement contenir que les endpoints pour plus d'adaptabilité.

gamecodeclub.com / api / v1 / games / {id} / reviews
└────┬──────────┘ └──────────────┬───────────┘
HOST ENDPOINT
┌──────┴──────┐
│ /api -> Indicateur d'API
│ /v1 -> Versioning
│ /games -> Ressource de base / Contexte
│ /{id} -> ID d'une ressource spécifique dans la collection.
│ /reviews -> Ressource spécifique

Par convention, le endpoint commence par /api et par un numéro de version (v1, v2...) qui permet à plusieurs versions de l'API de co-habiter.

S'en suivent des noms de ressources qui peuvent être enchaînées selon leur lien hiérarchique, avec éventuellement des indicateurs d'ID de resources :

  • Soit pour les cas où l'action s'applique à un élément :
    1. GET /games/{id} pour afficher le jeu qui a le numéro "ID"
    2. PUT /games/{id} pour mettre à jour la resource qui a ce numéro d'ID
    3. DELETE /games/{id} pour effacer la resource qui a ce numéro d'ID
  • Soit pour accéder à un élément, puis un élément sous sa propre hiérarchie :
    1. GET /games/{id}/reviews pour obtenir toutes les évaluations liée au jeu avec le numéro "id".
    2. POST /games/{id}/reviews pour ajouter une note sur le jeu avec le numéro "id".
    3. DELETE /games/{game-id}/reviews/{review-id} exemple d'effacement de la note "review-id" qui s'applique au jeu {game-id}.

À noter dans ce dernier exemple que certaines resources peuvent être accessible à plusieurs endroits. Pour prendre l'exemple de /reviews, nous pourrions avoir un endpoint /reviews qui accepte GET, PUT, DELETE, et dans le contexte de /games/{id}/reviews, proposer à la fois les verbes précédents, et en plus le verbe POST.

Cela ajoute une dimension contextuelle qui évite de réimplémenter certaines logiques côté client : pour cette exemple, c'est logique de pouvoir noter un jeu seulement au niveau d'un jeu, de lister les notes d'un jeu uniquement, ou de pouvoir noter absolument toutes les notes.

Dans ce cas de figure, une bonne pratique lors de la création sur un chemin "complexe" est que la réponse contienne le endpoint individuel de la review (review/{review-id}).

Verbe et CheminLogique fonctionnelle
GET /reviewsLister toutes les notes (exemple : page d'accueil du site où toutes les notes sont listées)
GET /games/{game-id}/reviewsLister toutes les notes d'un jeu en particulier
POST /games/{game-id}/reviewsEcrire une note d'un jeu en particulier
🚫 POST /reviewsCet endpoint n'a pas vraiment de sens

Les collections doivent toujours être au pluriel

C'est un questionnement qui se pose généralement lors de la création de la première version d'une API, et qui peut faire débat. Le premier point à prendre en compte est de respecter les normes existantes du projet : les API existantes dans le projet, les habitudes de l'entreprise et de l'équipe... Mais cela dit, la refonte d'une API ou la création d'une API peuvent être l'occasion de faire une mise au propre.

La question peut se poser car la plupart des verbes emmènent intuitivement vers du singulier. Il semble logique pour un POST ou un GET d'avoir un seul élément dans le endpoint. En plus, un des exemples d'API les plus courants, le "Pet Store" de OpenAPI, utilise du singulier par défaut.

Ce qui fait sens, et qui pousse la majorité à employer le pluriel, c'est que nous n'interrogeons pas un élément particulier, nous interrogeons une collection.

Ainsi, si je fais GET ou PUT /reviews/{:review-id}, je n'interroge pas un review avec un id, mais je demande dans la collection reviews, l'élément qui a reviews-id. La nuance est subtile, mais elle est là.

En raisonnant de cette manière, tous les éléments deviennent des collections qu'on interroge intégralement soit par élément particulier avec un id.

attention

Une pratique à éviter dans tous les cas : combiner le pluriel et le singulier pour un même endpoint. Ça complique l'implémentation pour le serveur comme pour le client, ça casse la prédictibilité des chemins de l'API, et surtout, ça ajoute une vraie complexité pour certains mots avec une forme plurielle irrégulière (data au pluriel est datum, city devient cities...)

Des endpoints au singulier :

Il y a cependant un cas où ça peut être utile d'avoir un endpoint au singulier : lorsque la ressource est toujours unique.

Voici quelques exemples :

  • Ressource globale du système : un endpoint /health ou /status (pour l'état du serveur) n'a pas de sens au pluriel.
  • Ressource spécifique à un élément collection : /user/86/cart pour voir
  • Ressource dans une collection directement. Exemple dans le cas d'une session : /user/self est l'équivalent de user/{id} avec l'utilisateur en cours.
  • Une action comme /reindex ou /run, qui peut être à la source ou dans un ressource selon les cas.

De même, outre une collection, une API peut aussi appartenir à un autre objet, au singulier.

EndpointTypeTraduction fonctionnelle
/gamesCollectionInterroger la collection Games - list d'objets games
/games/42Element de collectionInterroger le jeu avec l'ID 42
/games/42/reviewsCollectionInterroger la collection reviews pour le jeu avec l'ID 42, ou ajouter un élément dans la collection.
/games/42/installAction / SingletonLancer une action sur le jeu avec l'ID 42
'Points à retenir'
  • L'utilisation du pluriel ou du singulier apporte une signification.
  • Le pluriel indique qu'on travaille sur une collection, l'ID ajouté ensuite indique l'élément de la collections.
  • Le singulier indique une catégorie "flat" ou une action.
  • L'exemple d'API pet-store n'est pas une référence ! 🤯

Les paramètres d'URL (endpoint?key=value)

Un autre point qui peut se retrouver dans un endpoint, ce sont les paramètres d'URL.

Leur syntaxe est : ?key1=value1&key2=value2&key3=value3 (une série de clé-valeurs séparées par des esperluettes &).

L'usage de ces paramètres est à réserver au verbe GET, et généralement pour contrôler le filtrage d'une vue.

Un des usages courants est de sélectionner la page, ou de préciser une requête, pour faire transiter uniquement les données nécessaires.

Verbe + Endpoint http avec paramètresExplication
GET /games?limit=20&page=3Tous les jeux, vue limitée aux 2 premiers, 3e page de cette vue
GET /games?name_filter=OrbTous les jeux qui contiennent "Orb" dans le nom
GET /games?limit=20&page=3&name_filter=OrbCombinaison des deux filtres précédents
GET /games?sort=price_ascTri de la liste de jeux par ordre de prix croissant.
POST /games?name=Orb_and_the_Stars🚫 À prescrire : les paramètres ne doivent pas servir à créer un objet : utilisez plutôt une payload

N.B. : sur l'exemple, la pagination est explicite (limite et page). La bonne pratique consiste à prévoir une limite par défaut (et pouvant être modifiée par l'API au besoin, dans une certaine limite), pour éviter d'envoyer trop de données.

Par exemple le GET /games est équivalent à GET /games?limit=20&page=1 et ne renverra toujours que les 20 premiers résultats. Le client qui interroge l'API a la possibilité de faire varier la limite et la page en cours.

Cela évite d'afficher des centaines de résultats, et privilégie plutôt l'approche par filtres et recherches.

Comment lancer des actions via une API ?

Beaucoup d'applications sont centrées sur de la gestion de données, et n'ont donc besoin que d'opérations CRUD sur les ressources : des sous-systèmes font le reste.

Cependant, il y a des cas de figure où l'API doit servir à contrôler quelque chose. Lancer une simulation, démarrer une action, etc. Dans ce cas, quel verbe convient le mieux ?

Cas concret : nous avons un robot et nous pouvons piloter sa direction, sa vitesse et la couleur de ses LED.

Approche 1 : contrôle en direct avec PATCH

Dans la première approche, nous considérons que l'action doit se faire en direct et la réponse doit illustrer si ça a fonctionné :

Nous allons considérer que la ressource est l'état du mouvement du robot et utiliser le verbe PATCH, ou éventuellement PUT. L'avantage de PATCH dans ce contexte est qu'il ne nécessite pas de re-demander systématiquement l'état du robot pour le recréer.

Mise à jour d'une caractéristique du robot:

  • Endpoint : PATCH robot/{robot-id}/motion
  • Payload :
{
"direction": "east",
"speed": 1
}

Mise à jour de tout l'état du robot:

  • Endpoint : PUT robot/{robot-id}/motion
  • Payload (tout l'objet) :
{
"direction": "east",
"speed": 1,
"lightColor": "0x000000"
}

Retour de la part du robot

Avec ce type de commande, chaque envoi à l'API retourne si l'ordre a pu être appliqué ou non (voir codes de réponse), ce qui permet au client d'afficher en temps réel le résultat des demandes et l'état du robot.

Approche 2 : file d'attente d'actions POST

Cette approche est plutôt asynchrone. Nous utilisons le verbe POST pour créer une ressource. Dans ce cas là, nous ne créons pas un nouvel état pour le robot, nous lui donnons une directive, qui va éventuellement s'empiler dans une file de directive et sera traitée en asynchrone.

C'est notamment une bonne approche si on peut avoir plusieurs clients demandant le même ordre, ou si on ne peut garantir une réponse instantanée. Plutôt indiquée pour les tâches de fond.

Envoi d'un élément en file d'attente:

  • Endpoint : POST robot/{robot-id}/commands

Retour

  • Indique si l'élément est bien ajouté à la file d'attente (et éventuellement l'ordre ou le délai prévisionnel)

Questionnement du robot

  • Savoir l'état en cours : GET robot/{robot-id}/state
  • Connaître la file d'attente des commandes : GET robot/{robot-id}/commands
attention

Le retour ne doit pas indiquer que l'action est faite immédiatement car ce n'est pas vrai, et on ne doit pas non plus attendre que l'élément de la file d'attente soit traité pour envoyer le retour.

Dans un mode de fonctionnement comme celui-ci qui est asynchrone, le client doit être capable d'envoyer ses instructions avec POST, et utiliser des GET pour l'état actuel, voire pour les états à venir.