Patrones de diseño comunes

Respuestas vacías

El método Delete estándar debe mostrar google.protobuf.Empty, excepto cuando se borra de forma no definitiva, en cuyo caso el método debe mostrar el recurso con el estado actualizado para indicar que la eliminación está en curso.

En cuanto a los métodos personalizados, deben tener sus propios mensajes de 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 de tipo [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 API 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 de recursos. 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 API deben usar el patrón de diseño de etiquetas de recursos descrito en google.api.LabelDescriptor.

Para hacerlo, el diseño de la API debe 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, cuando se crea un recurso, ese recurso debe aparecer en los métodos LIST y GET, aunque el recurso debe indicar que no está listo para su uso. 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 los resultados de la lista en páginas) en un método de List, la API deberá hacer lo siguiente:

  • definir un page_token del campo de string 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 page_size del campo 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 el page_size es 0, el servidor decidirá la cantidad de resultados que se mostrarán.
  • definir un next_page_token del campo de string 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 siguiente página de resultados, el cliente deberá pasar el valor del next_page_token de la respuesta en la próxima llamada al método List (en el campo page_token del mensaje de solicitud):

rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);

message ListBooksRequest {
  string name = 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 de int32 denominado 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

NOTA: Se elige "-" en lugar de "*" para evitar la necesidad de la codificación de URL.

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 Get recupere 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 debe mostrar un recurso con un nombre como shelves/shelf713/books/book8141, en lugar de 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. Si quieres especificar un orden descendente para 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 iguales.

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 establece 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 tiene éxito, se debe mostrar google.rpc.Code.OK. Además, las solicitudes completas que usen el mismo mensaje de solicitud no deben mostrar google.rpc.Code.INVALID_ARGUMENT. Ten en cuenta que la solicitud aún 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.

Si hay un comportamiento predeterminado común, se debe usar el valor enum 0. Además, la API debe documentar el comportamiento esperado.

Si no hay un comportamiento predeterminado común, el valor enum 0 debe nombrarse ENUM_TYPE_UNSPECIFIED y se debe rechazar con 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 "}" ;

NOTA: TOKEN representa los símbolos de terminal definidos fuera de la gramática.

Tipos de número entero

En los diseños de API, los tipos de número entero sin firmar, como uint32 y fixed32, no deben usarse porque algunos lenguajes de programación y sistemas importantes, como Java, JavaScript y OpenAPI, no los admiten bien; por lo que es muy probable que causen 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 una llamada a la API de REST, existe un parámetro de consulta implícito del sistema, $fields, 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=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.

ETag

Un ETag es un identificador opaco que permite a un cliente realizar solicitudes condicionales. Para admitir identificadores ETag, una API debe incluir un campo de string etag en la definición del recurso. Además, su semántica debe coincidir con el uso común del ETag. Por lo general, 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
    • La RFC 2732 permite los caracteres no ASCII, sin embargo, 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 documentar.

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 error. 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 una entrada de solicitud, p. ej., un Book recuperado que luego se vuelve 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.

message Book {
  string name = 1;
  // Output only.
  Timestamp create_time = 2;
}

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 de User podría exponer la configuración por usuario como un singleton de 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.

¿Te ha resultado útil esta página? Enviar comentarios:

Enviar comentarios sobre...