Makina Blog

Le blog Makina-corpus

Design d'API pour app mobile


Recette d'une API qui rend happy.

Les besoins spécifiques de l'app mobile

Chez Makina Corpus, nous faisons des applications mobiles hybrides. Il n'est pas rare que nos clients gèrent eux-même la partie métier et qu'ils fournissent directement une interface pour accéder aux données. L'application mobile consommera donc une API (Application Programming Interface) servie par un serveur web. Le design de cette API est très important pour construire quelque chose qui conviendra aux développeurs : une API web peut être plus ou moins pratique, on pourra parler alors de DX, l'expérience développeur. Comme pour l'expérience utilisateur (UX), l'important peut être articulé autour des 3 U :

  1. Utile
  2. Utilisable
  3. Utilisée

Les ressources

L'API doit servir des ressources ; les ressources métier qui sont nécessaires aux différentes applications clientes qui la consommeront. Il faut bien repérer les ressources métier : ce n'est pas évident pour le développeur de l'application client qui ne connait pas forcément le métier de faire le point sur une API qui n'est pas bien écrite. En effet, le développeur de l'application qui consomme l'API n'a pas besoin d'avoir une connaissance poussée du métier, à l'instar de l'utilisateur.

Prenons l'exemple d'une application comme gitlab, un serveur pour gérer et héberger les projets git de votre équipe. Une des ressources facilement repérable est le projet et nous retrouverons donc deux routes autour de cette ressource :

Les filtres et recherches

Autour de ces deux routes, nous pourrons ajouter des query parameters qui vont permettre de donner plus de souplesse lors de nos appels en fonction de nos besoins spécifiques : filtrer, ordonner ou chercher sur cette ressource. On pourra par exemple appeler :

  • /projects?order_by=created_at qui permet d'ordonner la réponse par date de création ;
  • /projects?search=ionic qui retourne la liste des projets auxquels l'appelant à accès et qui contiennent le terme ionic.

On pourrait également imaginer utiliser les query parameters pour limiter les champs retournés par l'API (un peu à la manière de ce que propose la spécification GraphQL) :

  • /projects?fields=ssh_url_to_repo,owner,name pour récupérer le nom du projet, son responsable et un lien.

Les relations

Les relations devraient être des sous-ressources car il n'y a aucune raison que l'application cliente refasse les jointures de son côté. Ainsi on aura différentes routes permettant de récupérer les ressources relatives à un projet :

Là encore c'est grâce au token qu'on reconnait l'utilisateur qui en fait la demande et qu'on adapte la réponse en ne renvoyant que les données auxquelles l'appelant a le droit d'accéder. C'est pour ça qu'on parle d'API stateless, l'état n'est pas conservé par le serveur : chaque requête contient l'ensemble des éléments permettant de répondre.

De la même manière, on peut récupérer une sous-ressource particulière grâce à sa clef, son identifiant :

Il n'y a aucun problème à délivrer les mêmes ressources via plusieurs routes si cela a du sens. Prenons les membres d'un projet par exemple. Une user story pourrait être : en tant qu'administrateur je veux pouvoir administrer les informations des utilisateurs afin de modifier leurs données. Et une autre : en tant qu'utilisateur, je veux pouvoir voir les informations des membres d'une équipe d'un projet. On aura donc plusieurs routes qui délivrent la même ressource, mais pas forcément de la même manière :

En toute logique, pour un même user_id, les informations renvoyées sont les mêmes quelles que soit la route. Les champs seront différents si cela a une raison. Inutile de récupérer les champs website\_url d'un utilisateur pour afficher la liste des membres d'une équipe d'un projet. Par contre on pourra retrouver cette information dans l'application cliente en créant un lien, grâce à l'identifiant, vers la ressource principale.

Les verbes HTTP

Pour chacune des ressources, on utilisera les verbes HTTP pour faire des actions précises :

  • sur les ressources globales (les noms aux pluriels, ex : /projects) :
    • GET pour récupérer la liste des éléments ;
    • POST pour créer un nouvel élément.
  • sur les ressources particulières (les routes avec un identifiant, ex : /projects/338) :
    • GET pour récupérer les informations de cet élément ;
    • PUT ou PATCH pour modifier les informations de cet élément ;
    • DELETE pour supprimer cet élément.

Offline

Pour certaines applications mobiles, nous devons permettre à l'utilisateur de faire des actions même s'il n'a pas de connexion internet. Il n'est pas envisageable de monter une base de données synchronisée avec le serveur sur chacun des téléphones ou tablettes. Il n'est pas encore forcément possible d'utiliser les caches d'un Service Worker sur une application mobile hybride.

L'une des solutions possibles est donc de charger suffisamment d'informations depuis le serveur lorsqu'une connexion est disponible. En reprenant notre exemple gitlab, on pourra donc avoir une route /projects avec des query parameters particulier comme /projects?fields=all qui nous renverra l'ensemble des données des projets. Ainsi nous naviguerons dans notre application avec les données des projets nécessaires et suffisantes. Les données renvoyées dans un tel cas seront sous la forme d'un gros tableau json contenant les objets projects qui contriendront toute une structure de données arborescente permettant à l'application d'accéder aux sous-ressources d'un projet. Pour récupérer tous les commits des projets auxquels on a accès, pour faire une page d'accueil contenant l'activité récente, on pourra faire quelque chose comme ça :

var commits = projects.map(function(project) {
  return project.commits;
});

Ou encore, pour afficher le nombre d'événements pour l'ensemble des projets ou par projet, on pourra faire quelque chose comme :

var totalEvents = projects.reduce(function(count, project) {
  return count + project.events.length;
}, 0);
var events = projects.map(function(project) {
  return {
    name: project.name,
    count: project.events.length
  };
});

Pour l'afficher dans notre template ionic/angular de la sorte :

<div class="notification">
  <span class="notification__badge">{{totalEvents}}</span>
</div>

<ul>
  <li class="notification" ng-repeat="event in events">
    {{event.name}}
    <span class="notification__badge">{{event.count}}</span>
  </li>
</ul>

Sur une web app en ligne, nous appellerons les ressources quand nous en avons besoin, en acceptant qu'elle renvoie des informations complètes et auto-descriptives. Inutile de renvoyer une ressource en mode base de données relationnelle de ce type :

{
  "id": 12,
  "author_id": 13,
  "category": 0,
  "root": true
}

Les données renvoyées par l'API doivent être explicites et complètes. Les ressources doivent être logiquement organisées en fonction du métier, pas en fonction de la logique ORM (Object-Relational Mapping) ou de la base de données relationnelle utilisée. Les ressources ne doivent pas non plus être organisées en fonction de l'application tierce qui consommerait ces dernières. Il est toujours possible d'ajouter des query parameters pour spécifier une ressource en fonction d'un besoin particulier.

Les ressources doivent être des noms plutôt que des verbes. On écrira la route /projects plutôt que /getProjects. Les verbes seront réservés aux actions comme /search pour effectuer une recherche globale comme dans l'API de twitter.

Même si /projects renvoie tout ce qui est nécessaire pour un mode hors ligne, il faudra quand même construire les routes spécifiques aux ressources et sous-ressources. La requête POST /projects/{project_id}/members permettra d'\ ajouter un membre à un projet. La requête PUT /projects/{project_id}/hooks/{hook_id} permettra de modifier un hook pour un projet spécifique.

Dans le cadre d'une application hors ligne, on prendra soin de conserver ces requêtes de modification dans une pile qu'on videra lorsque la connexion sera disponible.

Pour aller plus loin

Vous pouvez retrouver tout un tas de très bons conseils sur le blog de Vinay Sahni qui me sert souvent de référence : Best Practices for Designing a Pragmatic RESTful API.

Dans une version plus complète, le livre blanc de apigee est aussi une très bonne référence : Web API Design: Crafting Interface that Developers Love (PDF).

Une bonne documentation pour votre API est souvent essentielle, quand je dois en faire, je la réalise en pseudo markdown avec la spécification opensource blueprint ou encore avec la spécification yaml de l'\ Open API Initiative basée sur swagger, lui aussi opensource. La documentation via ces deux spécifications permettent de générer une page html ainsi qu'un serveur mock.

Enfin si vous avez besoin de tester votre API, rien de tel qu'un petit client REST comme \</\> Rested.

Autre proposition : allez voir les API des gros services comme celle de gitlab, github, twitter, enchant, etc.

[Illustration: Peter Miller CC BY-NC-ND 2.0]

Actualités en lien

Image
Capture d'une partie de carte montrant un réseau de voies sur un fond de carte sombre. Au centre, une popup affiche les information de l'un des tronçons du réseau.
28/02/2024

Géné­rer un fichier PMTiles avec Tippe­ca­noe

Exemple de géné­ra­tion et d’af­fi­chage d’un jeu de tuiles vecto­rielles en PMTiles à partir de données publiques.

Voir l'article
Image
Read The Docs
01/02/2024

Publier une documentation VitePress sur Read The Docs

À l'origine, le site de documentation Read The Docs n'acceptait que les documentations Sphinx ou MKDocs. Depuis peu, le site laisse les mains libres pour builder sa documentation avec l'outil de son choix. Voici un exemple avec VitePress.

Voir l'article
Image
Django PostgreSQL
07/11/2023

Utiliser des fonctions PostgreSQL dans des contraintes Django

Cet article vous présente comment utiliser les fonctions et les check constraints PostgreSQL en tant que contrainte sur vos modèles Django.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus