Padrões comuns de design

Respostas vazias

O método Delete padrão precisa retornar google.protobuf.Empty, a menos que esteja executando uma exclusão "soft". Nesse caso, o método precisa retornar o recurso com o estado atualizado para indicar a exclusão em andamento.

Para métodos personalizados, eles precisam ter suas próprias mensagens XxxResponse, mesmo que estejam vazias, pois é muito provável que a funcionalidade delas cresça ao longo do tempo e precise retornar dados adicionais.

Como representar intervalos

Os campos que representam intervalos precisam usar intervalos semiabertos com a convenção de nomenclatura [start_xxx, end_xxx), como [start_key, end_key) ou [start_time, end_time). A semântica de intervalo semiaberto normalmente é usada pela biblioteca C++ STL e pela biblioteca padrão Java. As APIs precisam evitar o uso de outras formas de representar intervalos, como (index, count) ou [first, last].

Rótulos de recursos

Em uma API orientada a recursos, o esquema do recurso é definido pela API. Para permitir que o cliente anexe uma pequena quantidade de metadados simples aos recursos (por exemplo, ao marcar um recurso de máquina virtual como servidor de banco de dados), as APIs precisam adicionar um campo map<string, string> labels à definição do recurso:

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

Operações de longa duração

Se um método de API geralmente demora muito para ser concluído, ele pode ser projetado para retornar um recurso de Operação de longa duração ao cliente, que pode usá-lo para rastrear o progresso e receber o resultado. A Operação define uma interface padrão para trabalhar com operações de longa duração. As APIs individuais não podem definir suas próprias interfaces para essas operações para evitar inconsistências.

O recurso de operação precisa ser retornado diretamente como a mensagem de resposta, e qualquer consequência imediata da operação precisa ser refletida na API. Por exemplo, ao criar um recurso, esse recurso precisa aparecer nos métodos LIST e GET, embora o recurso precise indicar que ele não está pronto para uso. Quando a operação é concluída, o campo Operation.response precisa conter a mensagem que teria sido retornada diretamente, se o método não estivesse em execução longa.

Uma operação pode fornecer informações sobre seu progresso usando o campo Operation.metadata. Uma API precisa definir uma mensagem para esses metadados mesmo que a implementação inicial não preencha o campo metadata.

Paginação de lista

Os conjuntos listáveis devem ser compatíveis com paginação, mesmo que os resultados geralmente sejam pequenos.

Justificativa: se uma API não for compatível com a paginação desde o início, o suporte posterior será problemático porque a adição dessa funcionalidade interfere no comportamento da API. Os clientes que não sabem que a API agora usa paginação podem presumir incorretamente que receberam um resultado completo, quando na verdade só receberam a primeira página.

Para oferecer suporte à paginação (retornando resultados de lista em páginas) em um método List, a API precisa:

  • definir um page_token de campo string na mensagem de solicitação do método List. O cliente usa este campo para solicitar uma página específica dos resultados da lista;
  • definir um page_size de campo int32 na mensagem de solicitação do método List. Os clientes usam esse campo para especificar o número máximo de resultados a serem retornados pelo servidor. O servidor pode restringir ainda mais o número máximo de resultados retornados em uma única página. Se page_size for 0, o servidor decidirá o número de resultados a serem retornados.
  • definir um next_page_token de campo string na mensagem de resposta do método List. Esse campo representa o token de paginação para recuperar a próxima página de resultados. Se o valor for "", significa que não há mais resultados para a solicitação.

Para recuperar a próxima página de resultados, o cliente precisa transmitir o valor de next_page_token da resposta na chamada de método List subsequente (no campo page_token da mensagem de solicitação):

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

Quando os clientes passam em parâmetros de consulta além de um token de página, o serviço precisa apresentar falha na solicitação se os parâmetros da consulta não forem consistentes com o token de página.

O conteúdo de token de página deve ser um buffer de protocolo codificado em base64 seguro de URL. Isso permite que o conteúdo evolua sem problemas de compatibilidade. Se o token de página contiver informações potencialmente confidenciais, essas informações precisarão ser criptografadas. Os serviços precisam evitar que a adulteração de tokens de página exponha dados não intencionais por meio de um dos seguintes métodos:

  • exigir que os parâmetros de consulta sejam reespecificados nas solicitações de acompanhamento.
  • Apenas referenciar o estado da sessão ao lado do servidor no token da página.
  • Criptografar e assinar os parâmetros da consulta no token da página e revalidar e reautorizar esses parâmetros em cada chamada.

Uma implementação de paginação pode também fornecer a contagem total de itens em um campo int32 chamado total_size.

Listar subconjuntos

Às vezes, uma API precisa permitir um cliente List/Search entre subconjuntos. Por exemplo, a Library API tem uma coleção de estantes, e cada uma tem uma coleção de livros, e um cliente quer procurar um livro em todas as estantes. Nesses casos, recomenda-se o uso de List padrão na subcoleção e especifique o ID da coleção curinga "-" para as coleções-pai. Para o exemplo da Library API, podemos usar a solicitação da API REST a seguir:

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

Conseguir recursos exclusivos do subconjunto

Às vezes, um recurso dentro de um subconjunto tem um identificador que é exclusivo dos conjuntos pai. Nesse caso, pode ser útil permitir que um Get recupere esse recurso sem saber qual coleção-pai o contém. Nesses casos, é recomendável usar um Get padrão no recurso e especificar o ID de coleção curinga "-" para todos as coleções-pai nas quais o recurso é exclusivo. Por exemplo, na API Library, podemos usar a seguinte solicitação da API REST, se o livro for exclusivo entre todos os livros em todas as estantes:

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

O nome do recurso na resposta a esta chamada precisa usar o nome canônico do recurso, com identificadores reais do conjunto-pai em vez de "-" para cada um deles. Por exemplo, a solicitação acima precisa retornar um recurso com um nome como shelves/shelf713/books/book8141, não shelves/-/books/book8141.

Ordem de classificação

Se um método de API permite que o cliente especifique a ordem de classificação para os resultados da lista, a mensagem de solicitação deve conter um campo:

string order_by = ...;

O valor da string deve seguir a sintaxe SQL: lista de campos separada por vírgulas. Por exemplo, "foo,bar". A ordem de classificação padrão é ascendente. Para especificar a ordem decrescente de um campo, um sufixo " desc" precisa ser anexado ao nome do campo. Por exemplo: "foo desc,bar".

Caracteres de espaço redundantes na sintaxe são insignificantes. "foo,bar desc" e "  foo ,  bar  desc  " são equivalentes.

Validação da solicitação

Se um método de API tiver efeitos colaterais e houver necessidade de validar a solicitação sem que isso aconteça, a mensagem de solicitação conterá um campo:

bool validate_only = ...;

Se este campo estiver definido como true, o servidor não pode executar quaisquer efeitos colaterais e realizará somente a validação específica da implementação consistente com a solicitação completa.

Se a validação for bem-sucedida, google.rpc.Code.OK precisará ser retornado, e qualquer solicitação completa usando a mesma mensagem de solicitação não retornará google.rpc.Code.INVALID_ARGUMENT. Observe que a solicitação ainda pode falhar devido a outros erros, como google.rpc.Code.ALREADY_EXISTS ou devido a disputas.

Duplicação de solicitação

Para APIs de rede, é preferível usar métodos idempotentes da API, porque eles podem ser repetidos com segurança após falhas na rede. No entanto, alguns métodos de API não podem ser idempotentes com facilidade, como a criação de um recurso, e é necessário evitar duplicações desnecessárias. Para esses casos de uso, a mensagem de solicitação deve conter um código exclusivo, como um UUID, que o servidor usará para detectar duplicações e verificar se a solicitação é processada apenas uma vez.

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

Se uma solicitação duplicada for detectada, o servidor precisa retornar a resposta da solicitação que funcionou, porque o cliente provavelmente não recebeu a resposta anterior.

Valor padrão do enum

Cada definição do enum precisa começar com uma entrada de valor 0, que precisará ser usada quando um valor enum não for especificado explicitamente. O tratamento de valores 0 precisa ser documentado pelas APIs.

O valor de enumeração 0 precisa ser nomeado como ENUM_TYPE_UNSPECIFIED. Se existe um comportamento padrão comum, ele é usado quando um valor de enumeração não é especificado explicitamente. Se não existe um comportamento padrão comum, o valor 0 precisa ser rejeitado com o erro INVALID_ARGUMENT quando usado.

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;

Um nome idiomático pode ser usado para o valor 0. Por exemplo, google.rpc.Code.OK é a maneira idiomática de especificar a ausência de um código de erro. Nesse caso, OK é semanticamente equivalente a UNSPECIFIED no contexto do tipo do enum.

Nos casos em que há um padrão intrinsecamente sensível e seguro, esse valor pode ser usado para o valor '0'. Por exemplo, BASIC é o valor "0" no enum de Visualização de recursos.

Sintaxe de gramática

Em projetos de API, muitas vezes é necessário definir gramáticas simples para determinados formatos de dados, como a entrada de texto aceitável. Para fornecer uma experiência de desenvolvedor consistente em APIs e reduzir a curva de aprendizado, os designers da API precisam usar a seguinte variante da sintaxe do Formalismo de Backus-Naur Estendido (EBNF, na sigla em inglês) para definir essas 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úmeros inteiros

Nos designs de API, tipos inteiros sem sinal, como uint32 e fixed32 não podem ser usados porque algumas linguagens de programação e sistemas importantes não são compatíveis com eles, como Java, JavaScript e OpenAPI. Além disso, é mais provável que eles causem erros de transbordamento. Outra questão é que diferentes APIs são muito propensas a usar tipos com e sem assinatura incompatível para a mesma finalidade.

Quando os tipos de números inteiros com assinatura são usados para tarefas em que os valores negativos não são significativos, como tamanho ou tempo limite, o valor -1 (e somente -1) pode ser usado para indicar um significado especial, como final de arquivo (EOF, na sigla em inglês), tempo limite infinito, limite de cota ilimitado ou idade desconhecida. Esses usos precisam ser documentados com clareza para evitar confusão. Os produtores da API também documentarão o comportamento do valor padrão implícito 0 se não for muito óbvio.

Resposta parcial

Às vezes, um cliente da API só precisa de um subconjunto específico de dados na mensagem de resposta. Para serem compatíveis com esses casos de uso, algumas plataformas de API oferecem compatibilidade nativa para respostas parciais. A Plataforma de API do Google é compatível com a máscara de campo de resposta.

Para qualquer chamada da API REST, há um parâmetro de consulta do sistema $fields implícito, que é a representação JSON de um valor google.protobuf.FieldMask. A mensagem de resposta será filtrada pelos $fields antes de ser enviada de volta ao cliente. Essa lógica é tratada automaticamente para todos os métodos da API pela plataforma dela.

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

Visualização de recursos

Para reduzir o tráfego de rede, às vezes é útil permitir que o cliente limite as partes do recurso que o servidor retornará em suas respostas, retornando uma visualização do recurso em vez da representação completa dele. A compatibilidade de visualização de recursos em uma API é implementada com a adição de um parâmetro à solicitação do método, o que permite ao cliente especificar qual visão do recurso ele quer receber na resposta.

O parâmetro:

  • deve ser de um tipo enum
  • precisa ser nomeado como view

Cada valor da enumeração define quais partes do recurso (campos) serão retornados na resposta do servidor. Exatamente o que é retornado para cada valor view é definido pela implementação e precisa ser especificado na documentação da 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;
}

Essa estrutura será mapeada para URLs da seguinte maneira:

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

Você pode saber mais sobre a definição de métodos, solicitações e respostas no capítulo Métodos padrão deste Guia de projeto.

ETags

Uma ETag é um identificador opaco que permite que um cliente faça solicitações condicionais. Para suportar ETags, uma API precisa incluir um campo de string etag na definição do recurso e sua semântica precisa corresponder ao uso comum da ETag. Normalmente, etag contém a impressão digital do recurso calculado pelo servidor. Consulte a Wikipédia e o RFC 7232 para mais detalhes.

ETags podem ser forte ou fracamente validadas, sendo que ETags fracamente validadas são prefixadas com W/. Nesse contexto, uma validação forte significa que dois recursos com a mesma ETag têm conteúdo idêntico em todos os bytes e campos adicionais idênticos (ou seja, Content-Type). Isso significa que as ETags validadas de maneira forte permitem o armazenamento em cache de respostas parciais a serem montadas mais tarde.

Por outro lado, nos recursos que têm o mesmo valor da ETag validada de modo fraco, as representações são semanticamente equivalentes, mas não necessariamente idênticas em todos os bytes e, portanto, não são adequadas para o cache de resposta de solicitações de intervalo de bytes.

Exemplo:

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

É importante entender que as citações são realmente parte do valor da ETag e estarão presentes para se adequar ao RFC 7232. Isso significa que as representações JSON das ETags acabam escapando das aspas. Por exemplo, as ETags seriam representadas nos corpos de recursos JSON como:

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

Resumo dos caracteres permitidos nas ETags:

  • Somente ASCII para impressão
    • Caracteres não ASCII permitidos pela RFC 7232, mas de uso mais complexo para os desenvolvedores
  • Sem espaços
  • sem aspas duplas, além das posições indicadas acima
  • evitar barras invertidas conforme recomendado pelo RFC 7232 para evitar confusão com o escape

Campos de saída

As APIs podem distinguir entre os campos que são fornecidos pelo cliente como entradas e campos que só são retornados pelo servidor na saída em um recurso particular. Nos campos que são apenas de saída, o atributo de campo precisa ser anotado.

Se os campos de saída forem definidos na solicitação ou forem incluídos em um google.protobuf.FieldMask, o servidor precisará aceitar a solicitação sem erros. O servidor precisa ignorar a presença de campos somente de saída e qualquer indicação disso. O motivo dessa recomendação é que os clientes muitas vezes reutilizam recursos retornados pelo servidor como outra entrada de solicitação. Por exemplo, um Book recuperado será reutilizados posteriormente em um método UPDATE. Se forem validados campos somente de saída, isso acarretará trabalho extra ao cliente por conta da limpeza.

import "google/api/field_behavior.proto";

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

Recursos singleton

Um recurso singleton pode ser usado quando há apenas uma única instância de um recurso no recurso pai (ou dentro da API, se ele não tiver pai).

Os métodos Create e Delete padrão precisam ser omitidos para recursos singleton. O singleton é criado ou excluído implicitamente quando seu pai é criado ou excluído (e existe implicitamente se não tiver pai). O recurso precisa ser acessado usando os métodos Get e Update padrão, bem como quaisquer métodos personalizados apropriados para seu caso de uso.

Por exemplo, uma API com recursos User pode expor configurações por usuário como um 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;
}

Streaming de fechamento

Para qualquer API bidirecional ou de streaming de cliente, o servidor deve contar com o fechamento iniciado pelo cliente, conforme fornecido pelo sistema RPC, para concluir o fluxo do lado do cliente. Não é necessário definir uma mensagem de conclusão explícita.

Qualquer informação que o cliente precise enviar antes do fechamento precisa ser definida como parte da mensagem de solicitação.

Nomes com escopo de domínio

Um nome com escopo de domínio é um nome de entidade que tem um nome de domínio DNS como prefixo para evitar conflitos. Esse é um padrão de design útil quando diferentes organizações definem os nomes de entidades de maneira descentralizada. A sintaxe se assemelha a um URI sem um esquema.

Os nomes com escopo de domínio são amplamente usados entre as APIs do Google e do Kubernetes, como:

  • A representação do tipo Any do Protobuf: type.googleapis.com/google.protobuf.Any
  • Tipos de métricas do Stackdriver: compute.googleapis.com/instance/cpu/utilization
  • Chaves de marcadores: cloud.googleapis.com/location
  • Versões da Kubernetes API: networking.k8s.io/v1
  • O campo kind na extensão x-kubernetes-group-version-kind da OpenAPI

Bool X enum x string

Ao criar um método de API, é muito comum fornecer um conjunto de opções para um recurso específico, como ativar o rastreamento ou desativar o armazenamento em cache. A maneira comum de conseguir isso é introduzir um campo de solicitação do tipo bool, enum ou string. Nem sempre é óbvio qual é o tipo certo para usar em um determinado caso de uso. A opção recomendada é a seguinte:

  • Usar o tipo bool se quisermos ter um design fixo e intencionalmente não quiser estender a funcionalidade. Por exemplo, bool enable_tracing ou bool enable_pretty_print.

  • Usar um tipo enum se quisermos ter um design flexível, mas não esperar que ele mude com frequência. A regra geral é que a definição de enum só mudará uma vez por ano ou menos. Por exemplo, enum TlsVersion ou enum HttpVersion.

  • Usar o tipo string se tivermos um design aberto ou se o design puder ser alterado com frequência por um padrão externo. Os valores aceitos precisam ser claramente documentados. Exemplo:

Retenção de dados

Ao projetar um serviço de API, a retenção de dados é um aspecto crítico da confiabilidade do serviço. É comum que os dados do usuário sejam excluídos por engano por erros de software ou erros humanos. Sem a retenção de dados e a funcionalidade de cancelamento de exclusão correspondente, um simples erro pode causar um impacto comercial desastroso.

Em geral, recomendamos a seguinte política de retenção de dados para serviços de API:

  • Para metadados de usuários, configurações de usuários e outras informações importantes, deve haver retenção de dados durante 30 dias. Por exemplo, monitoramento de métricas, metadados de projetos e definições de serviços.

  • Para conteúdo de usuário de grande volume, deve haver retenção de dados por sete dias. Por exemplo, blobs binários e tabelas de banco de dados.

  • Para um estado temporário ou armazenamento caro, deve haver retenção de dados de um dia, se possível. Por exemplo, instâncias do Memcache e servidores Redis.

Durante a janela de retenção de dados, os dados podem ser descompilados sem perda de dados. Se for caro oferecer a retenção de dados gratuitamente, um serviço pode oferecer retenção de dados como uma opção paga.

Payloads grandes

As APIs em rede geralmente dependem de várias camadas de rede para o caminho dos dados. A maioria das camadas de rede tem limites rígidos no tamanho da solicitação e da resposta. 32 MB é um limite comumente usado em muitos sistemas.

Ao projetar um método de API que manipule payloads com mais de 10 MB, devemos escolher a estratégia certa de usabilidade e crescimento futuro. Para as APIs do Google, recomendamos usar o streaming ou download/upload de mídia para processar grandes payloads. Com o streaming, o servidor lida de maneira incremental com os grandes dados de maneira síncrona, como a API Cloud Spanner. Com a mídia, os grandes dados passam por um grande sistema de armazenamento, como o Google Cloud Storage, e o servidor pode processar os dados de forma assíncrona, como a API Google Drive.

Campos primitivos opcionais

Os buffers de protocolo v3 (proto3) têm suporte para os campos primitivos optional, que são semanticamente equivalentes aos tipos nullable em muitas linguagens de programação. Eles podem ser usados para distinguir valores vazios de valores não definidos.

Na prática, é difícil para os desenvolvedores lidar corretamente com campos opcionais. A maioria das bibliotecas de cliente HTTP JSON, incluindo bibliotecas de cliente das APIs do Google, não pode distinguir proto3 int32, google.protobuf.Int32Value e optional int32. Se um design alternativo é igualmente claro e não exige um primitivo opcional, prefira essa opção. Se você não usar campos opcionais, vai adicionar complexidade ou ambiguidade, então use campos primitivos opcionais. Os tipos de wrapper não podem ser usados no futuro. Em geral, os designers da API precisam usar tipos primitivos simples, como int32, para simplificar e manter a consistência.