Padrões comuns de projeto

Respostas vazias

O método Delete padrão precisa retornar google.protobuf.Empty, a menos que esteja executando uma exclusão reversível, caso em que o método precisa retornar o recurso com estado atualizado para indicar que o processo está em andamento.

Os métodos personalizados precisam ter suas próprias mensagens XxxResponse, mesmo que estejam vazias, porque é muito provável que as funcionalidades aumentem ao longo do tempo e eles precisem retornar mais dados.

Como representar intervalos

Em campos que representam intervalos, é necessário usar tipos semiabertos com 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. Nas APIs, é preciso evitar o uso de outras formas de representar intervalos, como (index, count) ou [first, last].

Rótulos de recursos

O esquema de recursos é definido pela API quando ela é orientada a recursos. Para que o cliente possa anexar uma pequena quantidade de metadados simples aos recursos, por exemplo, marcando um recurso de máquina virtual como servidor de banco de dados, é necessário usar nas APIs o padrão de design de rótulos de recursos descrito em google.api.LabelDescriptor.

Para isso, é necessário adicionar um campo map<string, string> labels à definição do recurso no projeto da API.

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 precisam definir suas próprias interfaces para essas operações a fim de 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 deve ser refletida na API. Por exemplo, ao criar um recurso, ele deve aparecer nos métodos LIST e GET, porém o recurso deve indicar que não está pronto para uso. Quando a operação é concluída, o campo Operation.response deve conter a mensagem retornada diretamente, se o método não fosse de longa duração.

Uma operação pode fornecer informações sobre seu progresso usando o campo Operation.metadata. É necessário definir uma mensagem na API para esses metadados mesmo que, na implementação inicial, o campo metadata não esteja preenchido.

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 ser compatível com paginação (retorno de resultados da lista em páginas) em um método List, a API precisa:

  • definir um campo string page_token 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 campo int32 page_size 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 campo de string next_page_token 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 é "", 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 posterior, no campo page_token da mensagem de solicitação:

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

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 sensíveis, essas informações deverã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 parâmetros de consulta sejam especificados novamente em 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 também pode informar a contagem total de itens em um campo int32 chamado total_size.

Listar subconjuntos

Às vezes, uma API precisa deixar um cliente List/Search em 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 usar a List padrão no subconjunto e especificar o código do conjunto curinga "-" para os conjuntos 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

OBSERVAÇÃO: o motivo para escolher "-" vez de "*" é evitar a necessidade de escape do URL.

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 Get recupere esse recurso sem saber qual conjunto pai o contém. Nesses casos, recomenda-se usar um padrão Get no recurso e especificar o código do conjunto curinga "-" para todos os que são pai dentro dos 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 conter o nome canônico dele, que inclui identificadores reais do conjunto pai em vez de "-" para cada conjunto pai. Por exemplo, a solicitação acima 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 em um campo, um sufixo " desc" precisa ser anexado ao nome dele. 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 esse campo for definido como true, nenhum efeito colateral poderá ser executado pelo servidor. Além disso, será feita somente uma validação específica da implementação compatível com a solicitação completa.

Se a validação funcionar, google.rpc.Code.OK precisará ser retornado, e qualquer solicitação completa com a mesma mensagem não poderá 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 por causa de condições de corrida.

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 é detectada, o servidor deve 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 deverá ser usada quando um valor não for especificado explicitamente. O tratamento de valores 0 precisa ser documentado pelas APIs.

Se houver um comportamento padrão comum, o valor do enum 0 precisa ser usado, e o comportamento esperado precisa ser documentado pela API.

Se não houver um comportamento padrão comum, o valor do enum 0, quando usado, precisa ser nomeado como ENUM_TYPE_UNSPECIFIED e rejeitado com o erro INVALID_ARGUMENT.

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

OBSERVAÇÃO: TOKEN representa os símbolos do terminal definidos fora da gramática.

Tipos de números inteiros

Nos projetos da API, tipos de números inteiros sem assinatura, como uint32 e fixed32, não podem ser usados porque não são compatíveis com algumas linguagens e sistemas de programação importantes, como Java, JavaScript e OpenAPI. Além disso, eles são mais propensos a causar erros de sobrecarga. 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 de sistema implícito $fields, 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=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:

  • precisa ser do tipo enum;
  • precisa ser chamado de view.

Cada valor da enumeração define quais partes do recurso (campos) serão retornados na resposta do servidor. O resultado a ser retornado para cada valor de 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 ser compatível com ETags, uma API precisa incluir um campo de string etag na definição do recurso, e a semântica dela precisa corresponder ao uso comum da ETag. Normalmente, a etag contém a impressão digital do recurso calculado pelo servidor. Consulte a Wikipédia e o RFC 7232 para mais detalhes.

As ETags podem ser validadas de maneira forte ou fraca (nesta última, 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.

Por 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:

  • apenas ASCII que possam ser impressos
    • caracteres não ASCII permitidos pelo RFC 2732, 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. Para os campos que são apenas de saída, o atributo de campo precisará ser documentado.

Observe que, se os campos somente de saída estiverem definidos na solicitação ou incluídos em google.protobuf.FieldMask, a solicitação precisará ser aceita pelo servidor sem gerar erros. O servidor precisa ignorar a presença de campos somente de saída e qualquer indicação disso. Isso é recomendado porque os clientes normalmente reutilizam recursos retornados pelo servidor como outra entrada de solicitação, por exemplo, um Book recuperado será posteriormente reutilizado em um método UPDATE. Se forem validados campos somente de saída, isso acarretará trabalho extra ao cliente por conta da limpeza.

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

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 padrão Create e Delete precisam ser omitidos para recursos singleton. O singleton é criado ou excluído implicitamente quando isso também acontece com o pai. Quando não houver pai, o singleton ainda existirá de forma implícita. O recurso precisa ser acessado usando os métodos padrão Get e Update, assim como quaisquer métodos personalizados apropriados para seu caso de uso.

Por exemplo, uma API com recursos User pode expor as 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 de tipo Any do Protobuf: type.googleapis.com/google.protobuf.Duration
  • Tipos de métrica do Stackdriver: compute.googleapis.com/instance/cpu/utilization
  • Chaves de rótulo: cloud.googleapis.com/location
  • Versões da API do Kubernetes: networking.k8s.io/v1
  • O campo de kind na extensão x-kubernetes-group-version-kind da OpenAPI.
Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…