Pattern di progettazione comuni

Risposte vuote

Il metodo Delete standard deve restituire google.protobuf.Empty, a meno che non stia eseguendo un'eliminazione temporanea, nel qual caso il metodo dovrebbe restituire la risorsa con il suo stato aggiornato per indicare l'eliminazione in corso.

Per i metodi personalizzati, devono avere i propri messaggi XxxResponse anche se sono vuoti, perché è molto probabile che la loro funzionalità cresca nel tempo e debba restituire dati aggiuntivi.

Rappresentazione di intervalli

I campi che rappresentano intervalli devono utilizzare intervalli semiaperti con la convenzione di denominazione [start_xxx, end_xxx), come [start_key, end_key) o [start_time, end_time). La semantica dell'intervallo semiaperto è comunemente utilizzata dalla libreria STL C++ e dalla libreria standard Java. Le API dovrebbero evitare di utilizzare altri modi per rappresentare gli intervalli, come (index, count) o [first, last].

Etichette risorse

In un'API orientata alle risorse, lo schema delle risorse è definito dall'API. Per consentire al client di collegare una piccola quantità di metadati semplici alle risorse (ad esempio, taggare una risorsa di macchina virtuale come server di database), le API devono aggiungere un campo map<string, string> labels alla definizione della risorsa:

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

Operazioni a lunga esecuzione

Se il completamento di un metodo API richiede in genere molto tempo, può essere progettato per restituire al client una risorsa di operazione a lunga esecuzione, che il client può utilizzare per monitorare l'avanzamento e ricevere il risultato. L'operazione definisce un'interfaccia standard per il funzionamento con operazioni a lunga esecuzione. Le singole API non devono definire le proprie interfacce per le operazioni a lunga esecuzione in modo da evitare incoerenze.

La risorsa dell'operazione deve essere restituita direttamente come messaggio di risposta e qualsiasi conseguenza immediata dell'operazione deve essere riportata nell'API. Ad esempio, quando crei una risorsa, questa dovrebbe apparire nei metodi LIST e GET, anche se la risorsa dovrebbe indicare che non è pronta per l'uso. Al termine dell'operazione, il campo Operation.response deve contenere il messaggio che sarebbe stato restituito direttamente se il metodo non fosse stato eseguito a lungo.

Un'operazione può fornire informazioni sul suo avanzamento utilizzando il campo Operation.metadata. Un'API deve definire un messaggio per questi metadati anche se l'implementazione iniziale non compila il campo metadata.

Impaginazione elenco

Le raccolte elencabili devono supportare l'impaginazione, anche se i risultati in genere sono ridotti.

Motivazione: se un'API non supporta l'impaginazione fin dall'inizio, supportarla in un secondo momento è problematica perché l'aggiunta di impaginazione interrompe il comportamento dell'API. I client che non sono consapevoli del fatto che l'API ora utilizza l'impaginazione potrebbero presumere erroneamente di aver ricevuto un risultato completo, mentre in realtà hanno ricevuto solo la prima pagina.

Per supportare l'impaginazione (restituzione dei risultati degli elenchi nelle pagine) in un metodo List, l'API dovrà:

  • definisci un campo string page_token nel messaggio di richiesta del metodo List. Il client utilizza questo campo per richiedere una pagina specifica dei risultati dell'elenco.
  • definire un campo int32 page_size nel messaggio di richiesta del metodo List. I client utilizzano questo campo per specificare il numero massimo di risultati che il server deve restituire. Il server potrebbe limitare ulteriormente il numero massimo di risultati restituiti in una singola pagina. Se page_size è 0, il server deciderà il numero di risultati da restituire.
  • definisci un campo string next_page_token nel messaggio di risposta del metodo List. Questo campo rappresenta il token di impaginazione per recuperare la pagina successiva dei risultati. Se il valore è "", significa che non ci sono ulteriori risultati per la richiesta.

Per recuperare la pagina di risultati successiva, il client passerà il valore di next_page_token della risposta nella successiva chiamata al metodo List (nel campo page_token del messaggio della richiesta):

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 i client passano parametri di ricerca oltre a un token di pagina, il servizio deve non riuscire la richiesta se i parametri di ricerca non sono coerenti con il token della pagina.

I contenuti del token della pagina devono essere un buffer di protocollo con codifica Base64 sicuro per l'URL. Ciò consente ai contenuti di evolversi senza problemi di compatibilità. Se il token di pagina contiene informazioni potenzialmente sensibili, queste informazioni dovrebbero essere criptate. I servizi devono impedire la manomissione con i token di pagina di esporre dati indesiderati tramite uno dei seguenti metodi:

  • richiedono di rispecificare parametri di ricerca nelle richieste di follow-up.
  • fanno riferimento solo allo stato della sessione lato server nel token della pagina.
  • crittografare e firmare i parametri di ricerca nel token della pagina, nonché riconvalidare e autorizzare nuovamente questi parametri a ogni chiamata.

Un'implementazione dell'impaginazione può fornire anche il numero totale di elementi in un campo int32 denominato total_size.

Elenco sottoraccolte

A volte, un'API deve consentire a un client di List/Search tra le raccolte secondarie. Ad esempio, l'API Library ha una raccolta di scaffali e ogni scaffale ha una raccolta di libri e un cliente vuole cercare un libro in tutti gli scaffali. In questi casi, ti consigliamo di utilizzare lo standard List nella raccolta secondaria e di specificare l'ID raccolta di caratteri jolly "-" per le raccolte padre. Per l'esempio dell'API Library, possiamo utilizzare la seguente richiesta API REST:

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

Ottieni risorsa unica dalla raccolta secondaria

A volte, una risorsa all'interno di una raccolta secondaria ha un identificatore univoco all'interno delle raccolte padre. In questo caso, potrebbe essere utile consentire a un Get di recuperare la risorsa senza sapere in quale raccolta padre la contiene. In questi casi, consigliamo di utilizzare uno standard Get nella risorsa e di specificare l'ID raccolta con caratteri jolly "-" per tutte le raccolte padre all'interno delle quali la risorsa è univoca. Ad esempio, nell'API Library possiamo utilizzare la seguente richiesta API REST, se il libro è univoco tra tutti i libri in tutti gli scaffali:

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

Il nome della risorsa nella risposta a questa chiamata deve utilizzare il nome canonico della risorsa, con identificatori effettivi della raccolta padre anziché "-" per ogni raccolta padre. Ad esempio, la richiesta precedente dovrebbe restituire una risorsa con un nome simile a shelves/shelf713/books/book8141, non shelves/-/books/book8141.

Ordinamento

Se un metodo API consente al client di specificare l'ordinamento per i risultati dell'elenco, il messaggio di richiesta deve contenere un campo:

string order_by = ...;

Il valore della stringa deve seguire la sintassi SQL: elenco di campi separati da virgole. Ad esempio: "foo,bar". L'ordinamento predefinito è crescente. Per specificare l'ordine decrescente di un campo, deve essere aggiunto un suffisso " desc" al nome del campo. Ad esempio: "foo desc,bar".

Gli spazi ridondanti nella sintassi non sono significativi. "foo,bar desc" e "  foo ,  bar  desc  " sono equivalenti.

Richiedi convalida

Se un metodo API ha effetti collaterali ed è necessario convalidare la richiesta senza causare questi effetti collaterali, il messaggio di richiesta deve contenere un campo:

bool validate_only = ...;

Se questo campo viene impostato su true, il server non deve eseguire effetti collaterali ed eseguire solo una convalida specifica per l'implementazione coerente con la richiesta completa.

Se la convalida ha esito positivo, deve essere restituito google.rpc.Code.OK e qualsiasi richiesta completa che utilizza lo stesso messaggio di richiesta non deve restituire google.rpc.Code.INVALID_ARGUMENT. Tieni presente che la richiesta potrebbe comunque non riuscire a causa di altri errori come google.rpc.Code.ALREADY_EXISTS o a causa di condizioni di gara.

Richiedi duplicazione

Per le API di rete, sono preferibili metodi API idempotenti, perché possono essere riprovati in sicurezza dopo errori di rete. Tuttavia, alcuni metodi dell'API non possono essere facilmente idempotenti, come la creazione di una risorsa, ed è necessario evitare duplicati non necessari. Per questi casi d'uso, il messaggio di richiesta deve contenere un ID univoco, ad esempio un UUID, che il server utilizzerà per rilevare la duplicazione e assicurarsi che la richiesta venga elaborata una sola volta.

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

Se viene rilevata una richiesta duplicata, il server deve restituire la risposta per la richiesta precedentemente riuscita, perché è molto probabile che il client non abbia ricevuto la risposta precedente.

Valore predefinito enum

Ogni definizione di enum deve iniziare con una voce con valore 0, che deve essere utilizzata quando un valore di enumerazione non è specificato esplicitamente. Le API devono documentare come vengono gestiti i valori 0.

Il valore di enumerazione 0 deve essere denominato ENUM_TYPE_UNSPECIFIED. Se esiste un comportamento predefinito comune, viene utilizzato quando un valore di enumerazione non è specificato esplicitamente. Se non esiste un comportamento predefinito comune, il valore 0 dovrebbe essere rifiutato con l'errore INVALID_ARGUMENT quando viene utilizzato.

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;

È possibile utilizzare un nome idiomatico per il valore 0. Ad esempio, google.rpc.Code.OK è il modo idiomatico per specificare l'assenza di un codice di errore. In questo caso, OK è semanticamente equivalente a UNSPECIFIED nel contesto del tipo di enumerazione.

Nei casi in cui esiste un valore predefinito intrinsecamente sensibile e sicuro, quel valore potrebbe essere utilizzato come valore "0". Ad esempio, BASIC è il valore "0" nell'enumerazione Visualizzazione risorse.

Sintassi grammaticale

Nelle progettazioni delle API, è spesso necessario definire semplici grammaticali per determinati formati di dati, ad esempio un input di testo accettabile. Per offrire agli sviluppatori un'esperienza coerente tra le API e ridurre la curva di apprendimento, i designer delle API devono utilizzare la seguente variante della sintassi EBNF (Extended Backus-Naur Form) per definire queste grammatiche:

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

Tipi di numeri interi

Nelle progettazioni di API, non devono essere utilizzati tipi interi senza firma come uint32 e fixed32 perché alcuni linguaggi e sistemi di programmazione importanti non li supportano bene, come Java, JavaScript e OpenAPI. Inoltre, è più probabile che causino errori di overflow. Un altro problema è che API diverse hanno molto probabilità di utilizzare tipi con e senza firma non corrispondenti per la stessa cosa.

Quando i tipi interi firmati vengono utilizzati per elementi in cui i valori negativi non sono significativi, ad esempio dimensioni o timeout, il valore -1 (e solo -1) può essere utilizzato per indicare un significato speciale, ad esempio fine del file (EOF), timeout infinito, limite di quota illimitato o età sconosciuta. Questi usi devono essere chiaramente documentati per evitare confusione. I producer di API devono anche documentare il comportamento del valore predefinito implicito 0, se non è molto evidente.

Risposta parziale

A volte un client API ha bisogno solo di un sottoinsieme specifico di dati nel messaggio di risposta. Per supportare questi casi d'uso, alcune piattaforme API forniscono supporto nativo per risposte parziali. La piattaforma API di Google lo supporta tramite una maschera dei campi di risposta.

Per qualsiasi chiamata API REST, è presente un parametro di query di sistema implicito $fields, che è la rappresentazione JSON di un valore google.protobuf.FieldMask. Il messaggio di risposta verrà filtrato in base a $fields prima di essere reinviato al client. Questa logica viene gestita automaticamente per tutti i metodi API dalla piattaforma API.

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

Visualizzazione risorse

Per ridurre il traffico di rete, a volte è utile consentire al client di limitare le parti della risorsa che il server deve restituire nelle sue risposte, restituendo una visualizzazione della risorsa anziché la rappresentazione completa della risorsa. Il supporto della visualizzazione delle risorse in un'API viene implementato aggiungendo un parametro alla richiesta del metodo, il che consente al client di specificare la visualizzazione della risorsa che vuole ricevere nella risposta.

Il parametro:

  • deve essere di tipo enum
  • deve essere denominato view

Ogni valore dell'enumerazione definisce le parti della risorsa (quali campi) verranno restituite nella risposta del server. Ciò che viene restituito esattamente per ogni valore view è definito dall'implementazione e deve essere specificato nella documentazione dell'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;
}

Questo costrutto verrà mappato a URL quali:

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

Per ulteriori informazioni sulla definizione di metodi, richieste e risposte, consulta il capitolo Metodi standard di questa Guida alla progettazione.

ETags

Un ETag è un identificatore opaco che consente a un client di effettuare richieste condizionali. Per supportare gli ETag, un'API deve includere un campo stringa etag nella definizione della risorsa e la sua semantica deve corrispondere all'uso comune di ETag. Normalmente, etag contiene l'impronta della risorsa calcolata dal server. Per ulteriori dettagli, consulta Wikipedia e RFC 7232.

Gli ETag possono essere convalidate con forza o deboli, dove gli ETag con convalida debole sono preceduti dal prefisso W/. In questo contesto, una convalida efficace significa che due risorse con lo stesso ETag hanno contenuti identici per byte per byte e campi aggiuntivi identici (ad esempio Content-Type). Ciò significa che gli ETag altamente convalidati consentono la memorizzazione nella cache di risposte parziali da assemblare in un secondo momento.

Al contrario, le risorse con lo stesso valore ETag convalidato debolmente indicano che le rappresentazioni sono semanticamente equivalenti, ma non necessariamente identiche byte per byte, e pertanto non sono adatte alla memorizzazione nella cache delle risposte delle richieste di intervallo di byte.

Ad esempio:

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

È importante capire che le virgolette fanno davvero parte del valore ETag e devono essere presenti per essere conformi a RFC 7232. Ciò significa che le rappresentazioni JSON degli ETag finiscono con l'escape delle virgolette. Ad esempio, gli ETag vengono rappresentati nei corpi delle risorse JSON come:

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

Riepilogo dei caratteri consentiti negli ETag:

  • Solo ASCII stampabile
    • Caratteri non ASCII consentiti da RFC 7232, ma meno facili da usare per gli sviluppatori
  • Nessuno spazio
  • Le virgolette doppie non sono ammesse se non nelle posizioni indicate sopra
  • Evita le barre rovesciate come consigliato da RFC 7232 per evitare confusione durante l'escape

Campi di output

Le API potrebbero voler distinguere tra i campi forniti dal client come input e i campi che vengono restituiti dal server solo al momento dell'output su una determinata risorsa. Per i campi che vengono solo di output, l'attributo del campo verrà annotato.

Tieni presente che se nella richiesta vengono impostati solo i campi di output o sono inclusi in un google.protobuf.FieldMask, il server deve accettare la richiesta senza errori. Il server deve ignorare la presenza di campi di solo output e qualsiasi relativa indicazione. Il motivo di questo suggerimento è che i client spesso riutilizzano le risorse restituite dal server come ulteriore input della richiesta, ad esempio un Book recuperato verrà successivamente riutilizzato in un metodo UPDATE. Se vengono convalidati solo i campi di output, ciò comporta un lavoro aggiuntivo al client per cancellare i campi solo di output.

import "google/api/field_behavior.proto";

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

Risorse singleton

Una risorsa singleton può essere utilizzata quando esiste una sola istanza di una risorsa all'interno della risorsa padre (o all'interno dell'API, se non è presente un'istanza padre).

I metodi standard Create e Delete devono essere omessi per le risorse singleton; il singleton viene creato o eliminato implicitamente quando viene creata o eliminata l'elemento padre (ed esiste implicitamente se non è presente un elemento padre). La risorsa deve essere accessibile utilizzando i metodi standard Get e Update, nonché gli eventuali metodi personalizzati appropriati per il tuo caso d'uso.

Ad esempio, un'API con risorse User potrebbe esporre le impostazioni per utente come 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;
}

Streaming a metà chiusura

Per qualsiasi API bidirezionale o di flusso client, il server deve affidarsi alla semichiusura avviata dal client, fornita dal sistema RPC, per completare il flusso lato client. Non è necessario definire un messaggio di completamento esplicito.

Tutte le informazioni che il client deve inviare prima della chiusura parziale devono essere definite come parte del messaggio di richiesta.

Nomi con ambito di dominio

Un nome con ambito di dominio è un nome di entità preceduto da un nome di dominio DNS per evitare conflitti di nomi. È un modello di progettazione utile quando organizzazioni diverse definiscono i nomi delle entità in modo decentralizzato. La sintassi è simile a un URI senza uno schema.

I nomi con ambito di dominio sono ampiamente utilizzati tra le API di Google e le API Kubernetes, ad esempio:

  • La rappresentazione del tipo di Protobuf Any: type.googleapis.com/google.protobuf.Any
  • Tipi di metriche di Stackdriver: compute.googleapis.com/instance/cpu/utilization
  • Chiavi etichetta: cloud.googleapis.com/location
  • Versioni API Kubernetes: networking.k8s.io/v1
  • Il campo kind nell'estensione OpenAPI x-kubernetes-group-version-kind.

Bool ed enum oppure stringa

Durante la progettazione di un metodo API, è molto comune fornire un insieme di opzioni per una funzionalità specifica, ad esempio abilitare il tracciamento o la disabilitazione della memorizzazione nella cache. Il modo più comune per raggiungere questo obiettivo è introdurre un campo di richiesta di tipo bool, enum o string. Non è sempre chiaro quale sia il tipo giusto da usare per un determinato caso d'uso. La scelta consigliata è la seguente:

  • Utilizzare il tipo bool se vogliamo avere un design fisso e non intendiamo estendere intenzionalmente la funzionalità. Ad esempio, bool enable_tracing o bool enable_pretty_print.

  • Utilizzare un tipo enum se vogliamo avere un design flessibile, ma non prevediamo che cambierà spesso. La regola generale è che la definizione di enum cambia solo una volta all'anno o meno spesso. Ad esempio, enum TlsVersion o enum HttpVersion.

  • Utilizzare il tipo string se abbiamo un design aperto oppure se il design può essere modificato frequentemente da uno standard esterno. I valori supportati devono essere documentati in modo chiaro. Ad esempio:

Conservazione dei dati

Durante la progettazione di un servizio API, la conservazione dei dati è un aspetto critico dell'affidabilità del servizio. È prassi comune che i dati utente vengano eliminati per errore a causa di bug del software o errori umani. Senza conservazione dei dati e funzionalità di annullamento dell'eliminazione corrispondenti, un semplice errore può causare un impatto aziendale catastrofico.

In generale, consigliamo le seguenti norme sulla conservazione dei dati per i servizi API:

  • Per i metadati utente, le impostazioni utente e altre informazioni importanti, i dati dovrebbero essere conservati per 30 giorni. Ad esempio, metriche di monitoraggio, metadati di progetto e definizioni di servizi.

  • Per i contenuti utente con volumi elevati, la conservazione dei dati deve durare 7 giorni. ad esempio BLOB binari e tabelle di database.

  • Per lo stato temporaneo o l'archiviazione costosa, dovrebbe essere prevista una conservazione dei dati di 1 giorno, se possibile. Ad esempio, istanze memcache e server Redis.

Durante il periodo di conservazione dei dati, è possibile annullare l'eliminazione senza perdere i dati. Se è costoso offrire la conservazione dei dati gratuitamente, un servizio può offrire la conservazione dei dati come opzione a pagamento.

Payload di grandi dimensioni

Le API in rete dipendono spesso da più livelli di rete per il loro percorso dati. La maggior parte dei livelli di rete prevede limiti rigidi per le dimensioni delle richieste e delle risposte. 32 MB è un limite di uso comune in molti sistemi.

Quando progettiamo un metodo API che gestisce payload di dimensioni superiori a 10 MB, dobbiamo scegliere con attenzione la strategia giusta per l'usabilità e la crescita futura. Per le API di Google, consigliamo di utilizzare il caricamento/download di flussi di dati o di contenuti multimediali per gestire payload di grandi dimensioni. Con i flussi, il server gestisce in modo incrementale i dati di grandi dimensioni in modo sincrono, ad esempio l'API Cloud Spanner. Con i contenuti multimediali, i dati di grandi dimensioni passano attraverso un sistema di archiviazione di grandi dimensioni, come Google Cloud Storage, e il server è in grado di gestire i dati in modo asincrono, ad esempio l'API Google Drive.

Campi primitivi facoltativi

Protocol Buffer v3 (proto3) supporta campi primitivi optional, che sono semanticamente equivalenti ai tipi nullable in molti linguaggi di programmazione. Possono essere utilizzati per distinguere i valori vuoti da quelli non impostati.

In pratica, è difficile per gli sviluppatori gestire correttamente i campi facoltativi. La maggior parte delle librerie client HTTP JSON, incluse le librerie client delle API di Google, non è in grado di distinguere i proto3 int32, google.protobuf.Int32Value e optional int32. Se un design alternativo è altrettanto chiaro e non richiede una primitiva facoltativa, scegli questa opzione. Se non utilizzi l'opzione opzionale, aggiungi complessità o ambiguità, utilizza campi primitivi facoltativi. In futuro non dovranno essere utilizzati più tipi di wrapper. In generale, per semplicità e coerenza, i progettisti di API devono usare tipi primitivi semplici, come int32.