Patrones de diseño comunes

Respuestas vacías

El método Delete estándar debería mostrar google.protobuf.Empty, a menos que esté realizando una eliminación "de forma no definitiva", en cuyo caso el método debería mostrar el recurso con su estado actualizado para indicar la eliminación en curso.

En cuanto a los métodos personalizados, deben tener sus propios mensajes XxxResponse incluso si están vacíos, ya que es muy probable que su funcionalidad aumente con el tiempo y deban mostrar datos adicionales.

Representación de rangos

Los campos que representan rangos deben usar intervalos semiabiertos con una convención de nombres [start_xxx, end_xxx), como [start_key, end_key) o [start_time, end_time). La biblioteca STL de C++ y la biblioteca estándar de Java suelen usar la semántica de intervalo semiabierto. Las APIs deben evitar el uso de otras formas de representar rangos, como (index, count) o [first, last].

Etiquetas de recursos

En una API orientada a recursos, la API define el esquema del recurso. Para permitir que el cliente adjunte una pequeña cantidad de metadatos simples a los recursos (por ejemplo, etiquetar un recurso de máquina virtual como un servidor de base de datos), las APIs deben agregar un campo map<string, string> labels a la definición del recurso:

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

Operaciones de larga duración

Si un método de API suele tardar mucho en completarse, se puede diseñar a fin de que muestre un recurso de operación de larga ejecución al cliente, que se puede usar para seguir el progreso y recibir el resultado. La Operación define una interfaz estándar para trabajar con operaciones de larga ejecución. Las API individuales no deben definir sus propias interfaces para operaciones de larga ejecución a fin de evitar inconsistencias.

El recurso de la operación se debe mostrar directamente como un mensaje de respuesta y cualquier consecuencia inmediata de la operación debe reflejarse en la API. Por ejemplo, al crear un recurso, ese recurso debería aparecer en los métodos LIST y GET, aunque el recurso debería indicar que no está listo para ser usado. Cuando se completa la operación, el campo Operation.response debe contener el mensaje que se habría mostrado directamente, si el método no fuera de larga duración.

Una operación puede proporcionar información sobre su progreso mediante el campo Operation.metadata. Una API debe definir un mensaje para estos metadatos incluso si la implementación inicial no propaga el campo de metadata.

Paginación de listas

Las colecciones que pueden ser enumeradas deben admitir la paginación, incluso si los resultados suelen ser pequeños.

Razonamiento: Si una API no admite la paginación desde el principio, incorporar la compatibilidad más tarde es problemático, ya que agregar paginación altera el comportamiento de la API. Los clientes que no saben que ahora la API usa la paginación podrían suponer, de forma incorrecta, que recibieron un resultado completo, cuando en realidad solo recibieron la primera página.

Para admitir la paginación (mostrar la lista de resultados en páginas) en un método List, la API deberá:

  • definir un campo string page_token en el mensaje de solicitud del método List. El cliente usa este campo para solicitar una página específica de los resultados de la lista.
  • definir un campo page_size de int32 en el mensaje de solicitud del método List Los clientes usan este campo para especificar la cantidad máxima de resultados que el servidor debe mostrar. El servidor puede limitar aún más la cantidad máxima de resultados que se muestran en una sola página. Si page_size es 0, el servidor decidirá la cantidad de resultados que se mostrarán.
  • definir un campo string next_page_token en el mensaje de respuesta del método List Este campo representa el token de paginación para recuperar la siguiente página de resultados. Si el valor es "", significa que no hay más resultados para la solicitud.

Para recuperar la página siguiente de resultados, el cliente deberá pasar el valor del next_page_token de la respuesta en la llamada de método List posterior (en el mensaje del mensaje de solicitud page_token):

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

Cuando los clientes, además de un token de la página, pasan parámetros de consulta, el servicio debe fallar la solicitud si los parámetros de consulta no son coherentes con el token de la página.

El contenido de los token de la página debe ser un búfer de protocolo codificado en base64 seguro para URL. Esto permite que el contenido evolucione sin problemas de compatibilidad. Si el token de la página contiene información que puede ser sensible, esa información debe estar encriptada. Los servicios deben evitar que la manipulación de tokens de la página exponga datos no deseados. Para ello, pueden hacer lo siguiente:

  • solicitar que los parámetros de consulta se vuelvan a especificar en las próximas solicitudes
  • solo hacer referencia al estado de la sesión del lado del servidor en el token de la página
  • encriptar y firmar los parámetros de consulta en el token de la página, y volver a validar y autorizar estos parámetros en cada llamada

Una implementación de la paginación también puede proporcionar el total de elementos en un campo int32 llamado total_size.

Enumera subcolecciones

A veces, una API debe permitir que un cliente haga una List/Search en subcolecciones. Por ejemplo, la API de Biblioteca tiene una colección de estantes, cada uno con una colección de libros, y un cliente quiere buscar un libro en los estantes. En esos casos, se recomienda usar la List estándar en la subcolección y especificar el ID comodín de colección "-" para las colecciones superiores. Para el ejemplo de la API de Biblioteca, podemos usar la siguiente solicitud de la API de REST:

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

Obtén un recurso único de la subcolección

A veces, un recurso en una subcolección tiene un identificador que es único dentro de las colecciones superiores. En este caso, puede ser útil permitir que un Getrecupere ese recurso sin saber qué colección superior lo contiene. En esos casos, se recomienda usar un Get estándar en el recurso y especificar el ID comodín de colección "-" para todas las colecciones superiores en las que el recurso es único. Por ejemplo, en la API de Biblioteca, podemos usar la siguiente solicitud de la API de REST, si el libro es único entre todos los libros de todos los estantes:

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

El nombre del recurso en la respuesta a esta llamada debe ser el nombre canónico del recurso, con los identificadores reales de la colección superior en lugar de "-" para cada colección superior. Por ejemplo, la solicitud anterior debería mostrar un recurso con un nombre como shelves/shelf713/books/book8141, no shelves/-/books/book8141.

Orden de clasificación

Si un método de API le permite al cliente especificar el orden de clasificación para los resultados de la lista, el mensaje de solicitud debe contener el siguiente campo:

string order_by = ...;

El valor de string debe seguir la sintaxis de SQL: lista de campos separados por comas. Por ejemplo: "foo,bar" El orden de clasificación predeterminado es ascendente. Para especificar el orden descendente de un campo, se debe agregar un sufijo " desc"al nombre del campo. Por ejemplo: "foo desc,bar".

Los caracteres de espacio redundantes en la sintaxis son insignificantes. "foo,bar desc" y "  foo ,  bar  desc  " son equivalentes.

Validación de la solicitud

Si un método de API tiene efectos secundarios y es necesario validar la solicitud sin generarlos, el mensaje de solicitud debe contener el siguiente campo:

bool validate_only = ...;

Si este campo se configura como true, el servidor no debe ejecutar ningún efecto secundario y solo debe realizar una validación específica de la implementación que sea coherente con la solicitud completa.

Si la validación se realiza correctamente, se debe mostrar google.rpc.Code.OK, y cualquier solicitud completa que use el mismo mensaje de solicitud no debe mostrar google.rpc.Code.INVALID_ARGUMENT. Ten en cuenta que la solicitud puede fallar debido a otros errores, como google.rpc.Code.ALREADY_EXISTS, o a condiciones de carrera.

Duplicación de la solicitud

Para las API de red, se prefieren los métodos de API idempotentes, ya que se pueden reintentar de manera segura después de fallas en la red. Sin embargo, lograr la idempotencia no es fácil en algunos métodos de API, como crear un recurso, y existe la necesidad de evitar las duplicaciones innecesarias. Para estos casos prácticos, el mensaje de solicitud debe contener un ID único, como un UUID, que el servidor puede usar para detectar la duplicación y asegurarse de que la solicitud solo se procese una vez.

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

Si se detecta una solicitud duplicada, el servidor debe mostrar la respuesta de la última solicitud exitosa, ya que es muy probable que el cliente no haya recibido la anterior.

Valor predeterminado enum

Cada definición enum debe comenzar con una entrada de valor 0, que se debe usar cuando no se especifique un valor enum de manera explícita. Las API deben documentar cómo se manejan los valores 0.

El valor enum 0 debe llamarse ENUM_TYPE_UNSPECIFIED. Si hay un comportamiento predeterminado común, se debe usar cuando no se especifique un valor enum de manera explícita. Si no hay un comportamiento predeterminado común, el valor 0 se debe rechazar y mostrar el error INVALID_ARGUMENT cuando se use.

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;

Se puede usar un nombre idiomático para el valor 0. Por ejemplo, google.rpc.Code.OK es la forma idiomática de especificar la ausencia de un código de error. En este caso, OK es semánticamente equivalente a UNSPECIFIED en el contexto del tipo enum.

En los casos en que haya un valor predeterminado, que sea razonable y seguro de forma intrínseca, ese valor se puede usar para el valor “0”. Por ejemplo, BASIC es el valor “0” en la enum de Vista de recursos.

Sintaxis gramatical

En los diseños de API, suele ser necesario definir gramáticas simples para ciertos formatos de datos, como las entradas de texto aceptables. Si quieres proporcionar una experiencia de desarrollador coherente en las API y reducir la curva de aprendizaje, los diseñadores de API deben usar la siguiente variante de la sintaxis de la Notación de Backus-Naur extendido (EBNF) para definir esas gramáticas:

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

Tipos de número entero

En los diseños de API, los tipos de números enteros sin firma, como uint32 y fixed32, no deben usarse porque algunos lenguajes y sistemas de programación importantes no los admiten bien, como Java, JavaScript y OpenAPI. Además, es más probable que provoquen errores de desbordamiento. Otro problema es que es muy probable que las diferentes API usen tipos firmados y sin firmar que no coincidan para la misma tarea.

Cuando se usan tipos de número entero firmados para tareas en las que los valores negativos no son significativos, como en el tamaño o el tiempo de espera, el valor -1 (y solo -1 ) se puede usar para indicar un significado especial, como final del archivo (EOF), tiempo de espera infinito, cuota sin límite o edad desconocida. Esos usos se deben documentar con claridad para evitar confusiones. Los productores de API también deben documentar el comportamiento del valor predeterminado implícito 0 si no es muy obvio.

Respuesta parcial

A veces, un cliente de la API solo necesita un subconjunto específico de datos en el mensaje de respuesta. A fin de asistir en estos casos prácticos, algunas plataformas de API proporcionan soporte nativo para respuestas parciales. La plataforma de la API de Google lo hace a través de una máscara de campo de respuesta.

Para cualquier llamada a la API de REST, hay un parámetro de consulta $fields implícito del sistema, que es la representación JSON de un valor google.protobuf.FieldMask. Se usa $fields para filtrar el mensaje de respuesta antes de enviarlo de vuelta al cliente. Esta lógica se maneja automáticamente para todos los métodos de API a través de la plataforma de la API.

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

Vista de recursos

Para reducir el tráfico de red, suele ser útil permitir que el cliente defina las partes del recurso que el servidor debería mostrar en sus respuestas, a fin de mostrar una vista del recurso en lugar de una representación completa de él. Puedes implementar la compatibilidad con la vista de recursos en una API si agregas un parámetro a la solicitud de método que permite al cliente especificar qué vista del recurso quiere ver en la respuesta.

El parámetro:

  • debe ser de tipo enum
  • debe llamarse view

Cada valor de la enumeración define las partes del recurso (los campos) que se mostrarán en la respuesta del servidor. Lo que se muestra para cada valor de view está definido por la implementación y se debe especificar en la documentación de la 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;
}

Esta construcción se asignará a distintas URL, por ejemplo:

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

Puedes obtener más información sobre la definición de métodos, solicitudes y respuestas en el capítulo Métodos estándar de esta guía de diseño.

ETags

Un ETag es un identificador opaco que permite a un cliente realizar solicitudes condicionales. Para admitir ETags, una API debe incluir un campo de string etag en la definición del recurso, y su semántica debe coincidir con el uso común de ETag. Normalmente, etag contiene la huella digital del recurso procesado por el servidor. Consulta Wikipedia y la RFC 7232 para obtener más detalles.

Los ETag se pueden validar de forma fuerte o débil. Los ETag que se validan de forma débil tienen el prefijo W/. En este contexto, una validación fuerte significa que dos recursos que tienen el mismo ETag poseen un contenido idéntico byte a byte y campos adicionales idénticos (es decir, tipo de contenido). Por lo tanto, los ETag que se validan de forma fuerte permiten que el almacenamiento en caché de respuestas parciales se agrupe más adelante.

A la inversa, si los recursos tienen el mismo valor de ETag validado de forma débil, las representaciones serán equivalentes en términos semánticos, pero no necesariamente idénticas byte a byte. Por lo tanto, no son adecuadas para el almacenamiento en caché de respuestas de solicitudes de rango de bytes.

Por ejemplo:

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

Es importante comprender que las comillas forman parte del valor de ETag y deben estar presentes para cumplir con la RFC 7232. Por lo tanto, las representaciones JSON de ETag escapan las comillas. Por ejemplo, los ETag se representarían en los cuerpos de recursos JSON de la siguiente manera:

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

Resumen de los caracteres permitidos en los ETag:

  • Solo ASCII imprimible
    • Caracteres no ASCII permitidos por la RFC 7232, pero presentan mayor dificultad para el desarrollador
  • Sin espacios
  • Sin comillas dobles, excepto que sean como las que se muestran arriba
  • Evita las barras invertidas según lo recomendado por la RFC 7232 a fin de evitar confusiones sobre el escape

Campos de salida

Es posible que las API quieran distinguir entre los campos que proporciona el cliente como entradas y los que solo muestra el servidor como salida en un recurso en particular. Para los campos que son solo de salida, el atributo del campo se debe anotar.

Ten en cuenta que si los campos de solo salida se configuran en la solicitud o se incluyen en un google.protobuf.FieldMask, el servidor debe aceptar la solicitud sin errores. El servidor debe ignorar la presencia de campos de solo salida y cualquier indicio de ellos. El motivo de esta recomendación es que los clientes a menudo vuelven a usar los recursos que muestra el servidor como otra entrada de solicitud, p.ej., un Book recuperado se volverá a usar en un método UPDATE. Si se validan los campos de solo salida, se agrega el trabajo adicional al cliente de borrar los campos de solo salida.

import "google/api/field_behavior.proto";

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

Recursos de singleton

Se puede usar un recurso de singleton cuando solo existe una instancia de un recurso dentro de su recurso superior (o dentro de la API, si no tiene un superior).

Los métodos estándar Create y Delete se deben omitir para los recursos singleton. El singleton se crea o se borra de forma implícita cuando su superior se crea o se borra (y existe de forma implícita si no tiene un superior). Se debe acceder al recurso con los métodos estándar Get y Update, o con cualquier método personalizado que sea apropiado para el caso práctico.

Por ejemplo, una API con recursos User podría exponer la configuración por usuario como un 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;
}

Transmisión de cierre parcial

Para las API bidireccionales o de transmisión de clientes, el servidor debe basarse en el cierre parcial iniciado por el cliente, proporcionado por el sistema de RPC, para completar la transmisión del lado del cliente. No es necesario definir un mensaje de finalización explícito.

Cualquier información que el cliente necesite enviar antes del cierre parcial debe definirse como parte del mensaje de solicitud.

Nombres con alcance de dominio

Un nombre con alcance de dominio es un nombre de entidad que tiene el prefijo de un nombre de dominio DNS para evitar colisiones de nombres. Es un patrón de diseño útil cuando diferentes organizaciones definen sus nombres de entidad de manera descentralizada. La sintaxis se parece a un URI sin un esquema.

Los nombres con alcance de dominio se usan ampliamente entre las API de Google y las API de Kubernetes, como las siguientes:

  • Representación del tipo Any de Protobuf: type.googleapis.com/google.protobuf.Any
  • Tipos de métricas de Stackdriver: compute.googleapis.com/instance/cpu/utilization
  • Claves de etiquetas: cloud.googleapis.com/location
  • Versiones de la API de Kubernetes: networking.k8s.io/v1
  • El campo kind en la extensión OpenAPI x-kubernetes-group-version-kind.

Bool vs. Enum vs. String

Cuando se diseña un método de API, es muy común proporcionar un conjunto de opciones para una función específica, como habilitar el seguimiento o inhabilitar el almacenamiento en caché. La forma común de lograrlo es introducir un campo de solicitud de tipo bool, enum o string. No siempre es evidente cuál es el tipo correcto para un caso de uso determinado. La opción recomendada es la siguiente:

  • Usar el tipo bool si queremos tener un diseño fijo y no queremos extender la funcionalidad de forma intencional. Por ejemplo, bool enable_tracing o bool enable_pretty_print.

  • Usar un tipo enum si queremos tener un diseño flexible, pero no esperamos que el diseño cambie con frecuencia La regla general es que la definición enum solo cambiará una vez al año o con menos frecuencia. Por ejemplo, enum TlsVersion o enum HttpVersion.

  • Usar el tipo string si tenemos un diseño abierto o si un estándar externo puede cambiar el diseño con frecuencia Los valores admitidos deben estar claramente documentados. Por ejemplo:

Retención de datos

Cuando se diseña un servicio de API, la retención de datos es un aspecto fundamental de la confiabilidad del servicio. Es común que los datos del usuario se borren por error debido a errores de software o errores humanos. Sin la retención de datos ni la funcionalidad de recuperación correspondiente, un simple error puede causar un impacto catastrófico en la empresa.

En general, recomendamos la siguiente política de retención de datos para servicios de la API:

  • Para los metadatos del usuario, la configuración del usuario y otra información importante, debe haber una retención de datos de 30 días. Por ejemplo, métricas de supervisión, metadatos del proyecto y definiciones de servicios.

  • En el caso del contenido de usuarios de gran volumen, la retención de datos debería ser de 7 días. Por ejemplo, BLOB binarios y tablas de bases de datos

  • En el caso del estado transitorio o el almacenamiento costoso, debe haber retención de datos de 1 día si es posible. Por ejemplo, las instancias de Memcache y los servidores de Redis.

Durante la ventana de retención de datos, los datos se pueden recuperar sin perder. Si es costoso ofrecer la retención de datos de forma gratuita, un servicio puede ofrecer retención de datos como una opción pagada.

Cargas útiles grandes

Las APIs conectadas en red a menudo dependen de varias capas de red para su ruta de datos. La mayoría de las capas de red tienen límites estrictos en cuanto al tamaño de las solicitudes y las respuestas. 32 MB es un límite de uso general en muchos sistemas.

Al diseñar un método de API que controle cargas útiles superiores a 10 MB, debemos elegir cuidadosamente la estrategia adecuada para la usabilidad y el crecimiento futuro. Para las APIs de Google, recomendamos usar la transmisión o la carga y descarga de contenido multimedia para controlar cargas útiles grandes. Con la transmisión, el servidor controla de forma incremental los datos grandes de manera síncrona, como la API de Cloud Spanner. Con los medios, los datos de gran tamaño fluyen a través de un sistema de almacenamiento grande, como Google Cloud Storage, y el servidor puede manejar los datos de forma asíncrona, como la API de Google Drive.

Campos primitivos opcionales

Los búferes de protocolo v3 (proto3) admiten campos primitivos optional, que son semánticamente equivalentes a los tipos nullable en muchos lenguajes de programación. Se pueden usar para distinguir valores vacíos de valores no establecidos.

En la práctica, es difícil para los desarrolladores manejar correctamente los campos opcionales. La mayoría de las bibliotecas cliente HTTP de JSON, incluidas las bibliotecas cliente de la API de Google, no pueden distinguir int32, google.protobuf.Int32Value y optional int32 de proto3. Si un diseño alternativo es igual de claro y no requiere una primitiva opcional, es preferible que lo hagas. Si no se usa el campo opcional, se agrega complejidad o ambigüedad, utiliza campos primitivos opcionales. No se deben usar los tipos de wrapper en el futuro. En general, los diseñadores de API deben usar tipos primitivos simples, como int32, para mayor simplicidad y coherencia.