Modèles de conception courants

Réponses vides

La méthode Delete standard doit renvoyer google.protobuf.Empty, sauf si elle effectue une suppression réversible. Dans ce cas, la méthode doit renvoyer la ressource avec son état mis à jour pour indiquer la suppression en cours.

Pour les méthodes personnalisées, elles doivent disposer de leurs propres messages XxxResponse même s'ils sont vides, car il est très probable que leurs fonctionnalités se développeront avec le temps et qu'elles devront renvoyer des données supplémentaires.

Représenter des plages

Les champs qui représentent des plages doivent utiliser des intervalles semi-ouverts avec la convention d'attribution de noms [start_xxx, end_xxx), tels que [start_key, end_key) ou [start_time, end_time). La sémantique de l'intervalle semi-ouvert est couramment utilisée par la bibliothèque STL C++ et la bibliothèque standard Java. Les API doivent éviter d'utiliser d'autres moyens de représenter les plages, par exemple (index, count) ou [first, last].

Libellés de ressource

Dans une API orientée ressource, le schéma de ressource est défini par l'API. Pour permettre au client d'associer une petite quantité de métadonnées simples aux ressources (par exemple, l'ajout de tags à une ressource de machine virtuelle en tant que serveur de base de données), les API devraient ajouter un champ map<string, string> labels à la définition de la ressource :

message Book {
  string name = 1;
  map<string, string> labels = 2;
}

Opérations de longue durée

Si une méthode API prend généralement beaucoup de temps, elle peut être conçue pour renvoyer au client une ressource d'opération de longue durée, qu'il peut utiliser pour suivre la progression et recevoir le résultat. L'opération définit une interface standard de sorte qu'elle puisse être utilisée avec des opérations de longue durée. Les API individuelles ne doivent pas définir leurs propres interfaces pour les opérations de longue durée afin d'éviter les incohérences.

La ressource d'opération doit être renvoyée directement en tant que message de réponse et toute conséquence immédiate de l'opération doit être reflétée dans l'API. Par exemple, lors de la création d'une ressource, cette ressource doit s'afficher dans les méthodes LIST et GET, mais doit indiquer qu'elle n'est pas prête à être utilisée. Une fois l'opération terminée, le champ Operation.response doit contenir le message qui aurait été renvoyé directement, si la méthode n'était pas de longue durée.

Une opération peut fournir des informations sur sa progression à l'aide du champ Operation.metadata. Une API doit définir un message pour ces métadonnées même si la mise en œuvre initiale ne renseigne pas le champ metadata.

Pagination de liste

Les collections pouvant être listées devraient accepter la pagination, même si les résultats sont généralement peu volumineux.

Logique : Si une API n'est pas compatible avec la pagination dès le départ, sa compatibilité ultérieure est compromise, car l'ajout de la pagination perturbe le comportement de l'API. Les clients qui ne savent pas que l'API utilise maintenant la pagination peuvent supposer à tort qu'ils ont reçu un résultat complet, alors qu'en réalité ils n'ont reçu que la première page.

Pour accepter la pagination (renvoyer les résultats d'une liste en pages) dans une méthode List, l'API devra :

  • Définir un élément page_token pour le champ string dans le message de requête de la méthode List. Le client utilise ce champ pour demander une page spécifique des résultats de la liste.
  • Définir un élément page_size pour le champ int32 dans le message de requête de la méthode List. Les clients utilisent ce champ pour spécifier le nombre maximal de résultats à renvoyer par le serveur. Le serveur peut en outre limiter le nombre maximal de résultats renvoyés sur une seule page. Si la valeur de page_size est 0, le serveur décide du nombre de résultats à renvoyer.
  • Définir un élément next_page_token pour le champ string dans le message de requête de la méthode List. Ce champ représente le jeton de pagination permettant de récupérer la page suivante de résultats. Si la valeur est "", cela signifie qu'aucun autre résultat ne correspond à la requête.

Pour récupérer la page suivante de résultats, le client devra transmettre la valeur de l'attribut next_page_token de la réponse dans l'appel de méthode List suivant (dans le champ page_token du message de la requête) :

rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);

message ListBooksRequest {
  string parent = 1;
  int32 page_size = 2;
  string page_token = 3;
}

message ListBooksResponse {
  repeated Book books = 1;
  string next_page_token = 2;
}

Lorsque les clients transmettent des paramètres de requête en plus d'un jeton de page, le service doit faire échouer la requête si les paramètres de requête ne sont pas cohérents avec le jeton de page.

Le contenu du jeton de page devrait être un tampon de protocole encodé en base64 et sécurisé pour les URL. Cela permet au contenu d'évoluer sans problèmes de compatibilité. Si le jeton de page contient des informations potentiellement sensibles, ces informations doivent être chiffrées. Les services doivent empêcher une falsification des jetons de page, d'exposer des données non souhaitées en recourant à l'une des méthodes suivantes :

  • Exiger que les paramètres de requête soient respectés lors des requêtes de suivi.
  • Ne référencer que l'état de session côté serveur dans le jeton de page.
  • Chiffrer et signer les paramètres de requête dans le jeton de page, puis revalider et réautoriser ces paramètres à chaque appel.

Une mise en œuvre de la pagination peut également fournir le nombre total d'éléments dans un champ int32 nommé total_size.

Liste des sous-collections

Parfois, une API doit laisser un client List/Search dans plusieurs sous-collections. Par exemple, l'API de bibliothèque possède une collection d'étagères et chaque étagère inclut une collection de livres. Supposons qu'un client souhaite rechercher un livre dans toutes les étagères. Dans un tel cas, il est recommandé d'utiliser la méthode List standard sur la sous-collection et de spécifier le caractère générique d'ID de collection "-" pour la ou les collections parentes. Pour l'exemple d'API de bibliothèque, nous pouvons utiliser la requête d'API REST suivante :

GET https://library.googleapis.com/v1/shelves/-/books?filter=xxx

Obtenir des ressources uniques dans la sous-collection

Parfois, une ressource dans une sous-collection possède un identifiant unique dans sa ou ses collections parentes. Dans ce cas, il peut être utile d'autoriser une méthode Get à récupérer cette ressource sans savoir dans quelle collection parente elle se trouve. Dans un tel cas, il est recommandé d'utiliser la méthode Get standard sur la ressource et de spécifier le caractère générique d'ID de collection "-" pour toutes les collections parentes dans lesquelles la ressource est unique. Par exemple, dans l'API de bibliothèque, nous pouvons utiliser la requête API REST suivante, si le livre est unique parmi tous les livres de toutes les étagères :

GET https://library.googleapis.com/v1/shelves/-/books/{id}

Le nom de ressource dans la réponse à cet appel doit utiliser le nom canonique de la ressource avec les identifiants réels de la collection parente au lieu de "-" pour chaque collection parente. Par exemple, la requête ci-dessus doit renvoyer une ressource nommée shelves/shelf713/books/book8141 et pas shelves/-/books/book8141.

Ordre de tri

Si une méthode API permet au client de spécifier l'ordre de tri des résultats de la liste, le message de requête devrait contenir un champ :

string order_by = ...;

La valeur de chaîne devrait respecter la syntaxe SQL : liste de champs séparés par des virgules. Exemple : "foo,bar". L'ordre de tri par défaut est croissant. Afin de spécifier l'ordre décroissant pour un champ, un suffixe " desc" doit être ajouté au nom du champ. Exemple : "foo desc,bar".

Les espaces doubles dans la syntaxe sont sans conséquence. "foo,bar desc" et "  foo ,  bar  desc  " sont équivalents.

Validation de requête

Si une méthode API a des effets secondaires et qu'il est nécessaire de valider la requête sans entraîner de tels effets, le message de la requête devrait contenir le champ suivant :

bool validate_only = ...;

Si ce champ est défini sur true, le serveur ne doit pas exécuter d'effet secondaire. Il doit se contenter d'effectuer une validation spécifique à la mise en œuvre compatible avec la requête complète.

Si la validation aboutit, google.rpc.Code.OK doit être renvoyé et toute requête complète utilisant le même message de requête ne doit pas renvoyer google.rpc.Code.INVALID_ARGUMENT. Notez que la requête peut toujours échouer en raison d'autres erreurs telles que google.rpc.Code.ALREADY_EXISTS ou de conditions de concurrence.

Duplication de requête

Pour les API réseau, les méthodes API idempotentes sont largement préférées, car elles peuvent être retentées en toute sécurité après une défaillance du réseau. Cependant, certaines méthodes API ne peuvent pas être facilement idempotentes, comme la création de ressource, et il est nécessaire d'éviter les duplications inutiles. Dans de tels cas d'utilisation, le message de requête devrait contenir un ID unique, tel qu'un identifiant universel unique (UUID), que le serveur utilisera pour détecter les doublons et s'assurer que la requête n'est traitée qu'une fois.

// A unique request ID for server to detect duplicated requests.
// This field **should** be named as `request_id`.
string request_id = ...;

Si une requête en double est détectée, le serveur devrait renvoyer la réponse à la requête ayant précédemment abouti, car le client n'a probablement pas reçu la réponse précédente.

Valeur d'énumération par défaut

Chaque définition d'énumération doit commencer par une entrée dont la valeur est 0, qui devra être utilisée lorsqu'une valeur d'énumération n'est pas explicitement spécifiée. Les API doivent documenter la manière dont les valeurs 0 sont gérées.

La valeur d'énumération 0 devrait être nommée ENUM_TYPE_UNSPECIFIED. S'il existe un comportement commun par défaut, il devra être utilisé lorsqu'une valeur d'énumération n'est pas explicitement spécifiée. S'il n'y a pas de comportement commun par défaut, la valeur 0 devrait être rejetée avec l'erreur INVALID_ARGUMENT lorsqu'elle est utilisée.

enum Isolation {
  // Not specified.
  ISOLATION_UNSPECIFIED = 0;
  // Reads from a snapshot. Collisions occur if all reads and writes cannot be
  // logically serialized with concurrent transactions.
  SERIALIZABLE = 1;
  // Reads from a snapshot. Collisions occur if concurrent transactions write
  // to the same rows.
  SNAPSHOT = 2;
  ...
}

// When unspecified, the server will use an isolation level of SNAPSHOT or
// better.
Isolation level = 1;

Un nom idiomatique peut être utilisé pour la valeur 0. Par exemple, google.rpc.Code.OK correspond à la façon idiomatique de spécifier l'absence d'un code d'erreur. Dans ce cas, OK est sémantiquement équivalent à UNSPECIFIED dans le contexte du type d'énumération.

Dans les cas où une valeur par défaut intrinsèquement sensible et sûre existe, cette valeur peut être utilisée en tant que valeur "0". Par exemple, BASIC est la valeur "0" dans l'énumération Vue des ressources.

Syntaxe grammaticale

Dans les conceptions d'API, il est souvent nécessaire de définir des grammaires simples pour certains formats de données, telles que la saisie de texte acceptable. Pour offrir une expérience cohérente au développeur à travers les API et réduire la durée d'apprentissage, les concepteurs d'API doivent utiliser la variante suivante de la syntaxe EBNF (BNF étendu) pour définir ces grammaires :

Production  = name "=" [ Expression ] ";" ;
Expression  = Alternative { "|" Alternative } ;
Alternative = Term { Term } ;
Term        = name | TOKEN | Group | Option | Repetition ;
Group       = "(" Expression ")" ;
Option      = "[" Expression "]" ;
Repetition  = "{" Expression "}" ;

Types entiers

Dans les conceptions d'API, les types entiers non signés tels que uint32 et fixed32 ne doivent pas être utilisés, car ils ne sont pas compatibles avec certains langages de programmation et systèmes importants, tels que Java, JavaScript et OpenAPI. Ils sont plus susceptibles de causer des erreurs de dépassement. Un autre problème est que différentes API sont très susceptibles d'utiliser des types non concordants signés et non signés pour la même chose.

Lorsque des types entiers signés sont utilisés pour des éléments où les valeurs négatives ne sont pas significatives, telles que la taille ou le délai d'expiration, la valeur -1 (et -1 uniquement) peut être utilisée pour indiquer une signification particulière, par exemple la fin du fichier, un délai avant expiration infini, une limite de quota sans plafond ou un âge inconnu. De tels usages doivent être clairement documentés pour éviter toute confusion. Les producteurs d'API doivent également documenter le comportement de la valeur implicite par défaut 0 s'il n'est pas très évident.

Réponse partielle

Parfois, un client d'API n'a besoin que d'un sous-ensemble de données spécifique dans le message de réponse. Pour permettre de tels cas d'utilisation, certaines plates-formes d'API fournissent une compatibilité native avec les réponses partielles. La plate-forme d'API Google permet cela grâce au masque de champ de réponse.

Pour tout appel d'API REST, il existe un paramètre de requête système implicite $fields, qui est la représentation JSON d'une valeur google.protobuf.FieldMask. Le message de réponse sera filtré par $fields avant d’être renvoyé au client. Cette logique est gérée automatiquement pour toutes les méthodes API par la plate-forme d'API.

GET https://library.googleapis.com/v1/shelves?$fields=shelves.name
GET https://library.googleapis.com/v1/shelves/123?$fields=name

Vue des ressources

Pour réduire le trafic réseau, il est parfois utile de permettre au client de limiter les parties de la ressource que le serveur doit renvoyer dans ses réponses, en renvoyant une vue de la ressource plutôt qu'une représentation complète de la ressource. La compatibilité avec la vue des ressources dans une API est implémentée en ajoutant un paramètre à la requête de méthode qui permet au client de spécifier quelle vue de la ressource il souhaite recevoir dans la réponse.

Le paramètre :

  • doit être de type enum ;
  • doit être nommé view.

Chaque valeur de l'énumération définit les parties de la ressource (les champs) à renvoyer dans la réponse du serveur. Le contenu exact renvoyé pour chaque valeur view est défini par la mise en œuvre et doit être spécifié dans la documentation de l'API.

package google.example.library.v1;

service Library {
  rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) {
    option (google.api.http) = {
      get: "/v1/{name=shelves/*}/books"
    }
  };
}

enum BookView {
  // Not specified, equivalent to BASIC.
  BOOK_VIEW_UNSPECIFIED = 0;

  // Server responses only include author, title, ISBN and unique book ID.
  // The default value.
  BASIC = 1;

  // Full representation of the book is returned in server responses,
  // including contents of the book.
  FULL = 2;
}

message ListBooksRequest {
  string name = 1;

  // Specifies which parts of the book resource should be returned
  // in the response.
  BookView view = 2;
}

Cette construction sera mappée à des URL telles que :

GET https://library.googleapis.com/v1/shelves/shelf1/books?view=BASIC

Pour en savoir plus sur la définition des méthodes, des requêtes et des réponses, reportez-vous au chapitre Méthodes standards de ce guide de conception.

ETags

Un ETag est un identifiant opaque permettant à un client d'exécuter des requêtes conditionnelles. Pour accepter les ETags, une API doit inclure un champ de chaîne etag dans la définition de la ressource, et sa sémantique doit correspondre à l'utilisation courante de l'ETag. En règle générale, etag contient l'empreinte de la ressource calculée par le serveur. Pour plus d'informations, consultez Wikipédia et le protocole RFC 7232.

Les ETags peuvent être fortement ou faiblement validés. Les ETags faiblement validés sont précédés de W/. Dans ce contexte, une validation forte signifie que deux ressources ayant le même ETag ont à la fois un contenu identique (octet par octet) et des champs supplémentaires identiques (c'est-à-dire, un type de contenu). Cela signifie que les ETags fortement validés permettent l'assemblage ultérieur des réponses partielles mises en cache.

Inversement, des ressources ayant la même valeur ETag faiblement validée signifient que les représentations sont sémantiquement équivalentes, mais pas nécessairement identiques octet par octet et qu'elles ne conviennent donc pas pour la mise en cache de réponses à des requêtes de plage d'octets.

Exemple :

// This is a strong ETag, including the quotes.
"1a2f3e4d5b6c7c"
// This is a weak ETag, including the prefix and quotes.
W/"1a2b3c4d5ef"

Il est important de comprendre que les guillemets font vraiment partie de la valeur ETag et qu'ils sont obligatoires conformément au protocole RFC 7232. Cela signifie que les représentations JSON des ETags se terminent par un échappement des guillemets. Par exemple, les ETags seraient représentées comme suit dans les corps de ressources JSON :

// Strong
{ "etag": "\"1a2f3e4d5b6c7c\"", "name": "...", ... }
// Weak
{ "etag": "W/\"1a2b3c4d5ef\"", "name": "...", ... }

Résumé des caractères autorisés dans les ETags :

  • ASCII imprimables uniquement
    • Caractères non ASCII autorisés par le protocole RFC 7232, mais moins adaptés aux développeurs
  • Pas d'espaces
  • Pas de guillemets doubles dans les positions indiquées ci-dessus
  • Barres obliques inverses à éviter conformément aux recommandations du protocole RFC 7232 pour éviter toute confusion avec l'échappement

Champs de sortie

Les API peuvent vouloir faire la distinction entre les champs fournis par le client en tant qu'entrées et ceux renvoyés exclusivement par le serveur de sortie sur une ressource particulière. Pour les champs exclusivement de sortie, l'attribut de champ devra être annoté.

Notez que si des champs uniquement en sortie sont définis dans la requête ou inclus dans un google.protobuf.FieldMask, le serveur doit accepter la requête sans erreur. Le serveur doit ignorer la présence de champs uniquement en sortie et toute indication associée. Cette recommandation s'explique par le fait que les clients réutilisent souvent les ressources renvoyées par le serveur sous forme d'une autre entrée de requête. Par exemple, un objet Book extrait qui serait réutilisé ultérieurement dans une méthode UPDATE. Si les champs exclusivement de sortie sont validés, le client devra travailler davantage pour les effacer.

import "google/api/field_behavior.proto";

message Book {
  string name = 1;
  Timestamp create_time = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
}

Ressources uniques

Une ressource unique (singleton) peut être utilisée lorsqu'une seule instance de ressource existe dans sa ressource parente (ou dans l'API, si elle n'a pas de parent).

Les méthodes standards Create et Delete doivent être omises pour les ressources uniques. Le singleton est créé ou supprimé implicitement lorsque son parent est créé ou supprimé (et existe implicitement s'il n'a pas de parent). Vous devez accéder à la ressource à l'aide des méthodes standards Get et Update, ainsi que de toutes les méthodes personnalisées adaptées à votre cas d'utilisation.

Par exemple, une API avec des ressources User peut exposer les paramètres par utilisateur en tant que singleton Settings.

rpc GetSettings(GetSettingsRequest) returns (Settings) {
  option (google.api.http) = {
    get: "/v1/{name=users/*/settings}"
  };
}

rpc UpdateSettings(UpdateSettingsRequest) returns (Settings) {
  option (google.api.http) = {
    patch: "/v1/{settings.name=users/*/settings}"
    body: "settings"
  };
}

[...]

message Settings {
  string name = 1;
  // Settings fields omitted.
}

message GetSettingsRequest {
  string name = 1;
}

message UpdateSettingsRequest {
  Settings settings = 1;
  // Field mask to support partial updates.
  FieldMask update_mask = 2;
}

Fermeture partielle en streaming

Pour toutes les API bidirectionnelles ou de streaming client, le serveur devrait s'appuyer sur la fermeture partielle déclenchée par le client, telle que fournie par le système RPC, pour faire aboutir le streaming côté client. Il n'est pas nécessaire de définir un message d'achèvement explicite.

Toute information que le client doit envoyer avant la fermeture partielle doit être définie dans le message de requête.

Noms à l'échelle du domaine

Un nom à l'échelle du domaine est un nom d'entité préfixé par un nom de domaine DNS afin d'éviter les conflits de noms. Il s'agit d'un modèle de conception utile lorsque différentes organisations définissent leurs noms d'entité de manière décentralisée. La syntaxe ressemble à un URI sans schéma.

Les noms à l'échelle du domaine sont largement utilisés parmi les API Google et les API Kubernetes, par exemple :

  • La représentation du type Protobuf Any : type.googleapis.com/google.protobuf.Any
  • Les types de métriques Stackdriver : compute.googleapis.com/instance/cpu/utilization
  • Clés de libellés : cloud.googleapis.com/location
  • Versions de l'API Kubernetes : networking.k8s.io/v1
  • Le champ kind de l'extension OpenAPI x-kubernetes-group-version-kind.

Types de champ "bool", "Enum" et "String"

Lors de la conception d'une méthode API, il est très courant de fournir un ensemble de choix pour une fonctionnalité spécifique, comme l'activation du traçage ou la désactivation de la mise en cache. La méthode la plus courante consiste à insérer un champ de requête de type bool, enum ou string. Il n'est pas toujours évident de choisir le type approprié pour un cas d'utilisation donné. Le choix recommandé est le suivant :

  • Utilisez le type bool si vous souhaitez une conception fixe sans étendre la fonctionnalité. Par exemple, bool enable_tracing ou bool enable_pretty_print.

  • Utilisez un type enum si vous souhaitez une conception flexible et que vous n'envisagez pas de la modifier souvent. En règle générale, la définition d'énumération ne change qu'une fois par an ou moins souvent. Par exemple, enum TlsVersion ou enum HttpVersion.

  • Utilisez le type string si vous souhaitez une conception ouverte ou qui peut être fréquemment modifiée par une norme externe. Les valeurs acceptées doivent être clairement documentées. Exemple :

Conservation des données

Lors de la conception d'un service d'API, la conservation des données est un aspect essentiel de la fiabilité du service. Il est fréquent que les données utilisateur soient supprimées par erreur par des bugs logiciels ou des erreurs humaines. Sans la conservation des données et la fonctionnalité d'annulation de la suppression correspondante, une simple erreur peut avoir un impact commercial catastrophique.

En général, nous recommandons les règles de conservation de données suivantes pour les services d'API :

  • Pour les métadonnées, les paramètres utilisateur et d'autres informations importantes, les données doivent être conservées pendant 30 jours. C'est le cas, par exemple, des métriques de surveillance, des métadonnées de projet et des définitions de service.

  • Pour les contenus utilisateur volumineux, les données doivent être conservées pendant sept jours. C'est le cas, par exemple, des blobs binaires et des tables de base de données.

  • Pour un espace de stockage transitoire ou coûteux, les données doivent être conservées pendant un jour si possible. C'est le cas, par exemple, des instances Memcache et des serveurs Redis.

Pendant la période de conservation des données, la suppression des données peut être annulée sans aucune perte de données. S'il est trop coûteux de fournir gratuitement la conservation des données, un service peut proposer cette fonctionnalité en tant qu'option payante.

Charges utiles volumineuses

Les API en réseau dépendent souvent de plusieurs couches réseau pour leur chemin de données. La plupart des couches réseau présentent des limites strictes sur la taille de la requête et de la réponse. 32 Mo est une limite couramment utilisée dans de nombreux systèmes.

Lors de la conception d'une méthode API qui gère des charges utiles supérieures à 10 Mo, nous devons choisir soigneusement la stratégie appropriée pour faciliter l'utilisation et la croissance future. Pour les API Google, nous vous recommandons d'utiliser le streaming ou l'importation/le téléchargement de médias pour gérer les charges utiles volumineuses. Avec le streaming, le serveur gère les données volumineuses de manière incrémentielle, comme l'API Cloud Spanner. Avec les médias, les données volumineuses transitent par un système de stockage important, tel que Google Cloud Storage, et le serveur peut les gérer de manière asynchrone, comme l'API Google Drive.

Champs primitifs facultatifs

Protocol Buffers v3 (proto3) accepte les champs primitifs optional, qui sont sémantiquement équivalents aux types nullable dans de nombreux langages de programmation. Ils permettent de distinguer les valeurs vides des valeurs non définies.

Dans la pratique, il est difficile pour les développeurs de gérer correctement les champs facultatifs. La plupart des bibliothèques clientes JSON HTTP, y compris les bibliothèques clientes des API Google, ne peuvent pas distinguer int32, google.protobuf.Int32Value et optional int32 de proto3. Si une autre conception est tout aussi claire et ne nécessite pas de champ primitif facultatif, préférez-la. Si ne pas utiliser de champ facultatif ajoute de la complexité ou de l'ambiguïté, alors utilisez les champs primitifs facultatifs. Les types de wrapper ne doivent plus être utilisés. En règle générale, les concepteurs d'API doivent utiliser des types primitifs simples, tels que int32, pour plus de simplicité et de cohérence.