Olá, Habr!
Temos a tão esperada segunda edição de Desenvolvimento Web com Node e Express .
Como parte de nossa pesquisa sobre este tópico, encontramos um artigo conceitual sobre como projetar uma API da web a partir de um modelo, onde links para recursos são usados em vez de chaves e valores de banco de dados. Original - do blog do Google Cloud, bem-vindo em cat.
Quando estamos modelando informações, a questão chave é como definir o relacionamento e o relacionamento entre duas entidades. Descrever padrões observados no mundo real em termos de entidades e seus relacionamentos é uma ideia fundamental que remonta pelo menos à Grécia antiga. Ele também desempenha um papel fundamental na moderna indústria de TI.
Por exemplo, na tecnologia de banco de dados relacional, os relacionamentos são descritos usando uma chave estrangeira - um valor armazenado em uma linha de uma tabela e apontando para uma linha diferente, em outra tabela ou na mesma tabela.
É igualmente importante expressar relacionamentos na API. Por exemplo, em uma API de varejo, entidades de informação podem corresponder a clientes, pedidos, entradas de catálogo, carrinhos de compras e assim por diante. A API da conta bancária descreve a qual cliente uma determinada conta pertence, bem como a qual conta cada dívida ou crédito está associado.
O método mais comum usado por desenvolvedores de API para expressar relacionamentos é fornecer chaves de banco de dados ou proxies para eles nos campos dessas entidades associadas a essas chaves. No entanto, em pelo menos uma classe de API (orientada para web), existe uma alternativa preferível a essa abordagem: o uso de links da web.
De acordo com a Internet Engineering Task Force ( IETF), um link da web pode ser considerado uma ferramenta para descrever relações entre páginas na web. Os links da web mais famosos são aqueles que aparecem em páginas HTML e são incluídos em elementos de link ou âncora ou em cabeçalhos HTTP. Mas os links também podem aparecer em recursos de API e usá-los em vez de chaves estrangeiras reduz significativamente a quantidade de informações que o provedor de API precisa documentar adicionalmente e o usuário precisa estudar.
Um link é um elemento em um recurso da web que contém uma referência a outro recurso da web, bem como o nome do relacionamento entre os dois recursos. Uma referência a outra entidade é escrita em um formato especial chamado "identificador de recurso exclusivo" (URI), para o qual existe um padrão IETF... Neste padrão, a palavra "recurso" se refere a qualquer entidade apontada por um URI. O nome do tipo de relacionamento no link pode ser considerado igual ao nome da coluna do banco de dados que contém as chaves estrangeiras e o URI no link é o mesmo que o valor da chave estrangeira. Os mais úteis de todos os URIs são aqueles que fornecem informações sobre o recurso referenciado usando o protocolo da web padrão. Esses URIs são chamados de "Localizador Uniforme de Recursos" (URL), e o URL mais importante para APIs é o URL HTTP.
Embora os links não sejam amplamente usados em APIs, algumas APIs da web muito famosas ainda são baseadas em URLs de HTTP como um meio de representar relacionamentos. Estes são, por exemplo, API do Google Drivee a API GitHub . Por que é tão? Neste artigo, mostrarei como usar a API de chaves estrangeiras na prática, explicarei suas desvantagens em comparação com o uso de links e mostrarei como converter um design que usa chaves estrangeiras em um onde os links são usados.
Representando relacionamentos com chaves estrangeiras
Considere o popular aplicativo educacional de pet shop. Este aplicativo armazena registros para rastrear informações sobre animais de estimação e seus proprietários. Animais de estimação têm atributos como nome, espécie e raça. Os proprietários têm nomes e endereços. Cada animal está relacionado ao seu dono, e a relação reversa permite que você encontre todos os animais de estimação de um determinado dono.
Em um design baseado em chave típico, a API da loja de animais fornece dois recursos semelhantes a este:
O relacionamento entre Lassie e Joe é expresso assim: na visão de Lassie, Joe é designado como tendo um nome e significado correspondentes a "proprietário". A relação oposta não é expressa. O valor do proprietário é "98765", que é uma chave estrangeira. Esta é provavelmente a chave estrangeira real do banco de dados - isto é, estamos lidando com o valor da chave primária de alguma linha de alguma tabela do banco de dados. Mas, mesmo que a implementação da API mude ligeiramente os valores da chave, ela ainda se aproxima da chave estrangeira em suas características principais.
O valor "98765" não é muito adequado para uso direto do cliente. Nos casos mais comuns, o cliente precisa construir um URL usando esse valor e a documentação da API precisa descrever a fórmula para fazer essa conversão. Normalmente, isso é feito definindo um padrão de URI , como este:
/people/{person_id}
O relacionamento reverso - animais de estimação são propriedade do proprietário - também pode ser exposto à API implementando e documentando um dos seguintes padrões de URI (as diferenças são apenas estilísticas, não substantivo):
/pets?owner={person_id}
/people/{person_id}/pets
APIs projetadas dessa forma geralmente requerem que muitos padrões de URI sejam definidos e documentados. A linguagem mais popular para definir tais padrões não é aquela especificada na especificação IETF, mas OpenAPI (anteriormente conhecido como Swagger). Antes da versão 3.0, o OpenAPI não tinha uma maneira de especificar quais valores de campo podiam ser inseridos em quais modelos, portanto, parte da documentação tinha que ser escrita em linguagem natural e parte tinha que ser adivinhada pelo cliente. O OpenAPI 3.0 introduz uma nova sintaxe chamada "links" para resolver esse problema, mas dá um pouco de trabalho para usar esse recurso de forma consistente.
Portanto, por mais comum que seja esse estilo, ele exige que o fornecedor documente e o cliente aprenda e use um número significativo de padrões de URI que não estão bem documentados nas especificações atuais da API. Felizmente, existe uma opção melhor.
Representando relacionamentos usando links
E se os recursos mostrados acima fossem modificados da seguinte maneira:
A principal diferença é que, neste caso, os valores são expressos usando referências, não usando valores de chave estrangeira. Aqui, os links são escritos em JSON regular, no formato de pares nome / valor (há uma seção abaixo que discute outras abordagens para escrever links em JSON).
Observe que a relação inversa, ou seja, do animal de estimação para o proprietário, agora também está explicitamente implementada porque
Joel
um campo foi adicionado à visualização
"pets"
.
A mudança
"id"
para
"self"
essencialmente não é necessária ou importante, mas há um acordo que usar
"self"
identifica um recurso cujos atributos e relacionamentos são especificados por outros pares de nome / valor no mesmo objeto JSON.
"self"
É o nome registrado na IANA para esta finalidade.
Do ponto de vista da implementação, substituir todas as chaves do banco de dados por links deve ser bastante simples - o servidor converte todas as chaves estrangeiras do banco de dados em URLs, de modo que nada precisa ser feito no cliente - mas a própria API neste caso é bastante simplificada, e a conectividade entre o cliente e o servidor cai. Muitos padrões de URI que eram importantes no primeiro design não são mais necessários e podem ser removidos da especificação e documentação da API.
Agora, nada impede que o servidor altere o formato de novas URLs a qualquer momento sem afetar os clientes (é claro, o servidor deve continuar a cumprir todas as URLs formuladas anteriormente). A URL passada ao cliente pelo servidor precisará incluir a chave primária da entidade especificada no banco de dados, além de algumas informações de roteamento. Mas, como o cliente simplesmente repete a URL enquanto responde ao servidor, e o cliente nunca precisa analisar a URL, os clientes não precisam saber como formatar a URL. Como resultado, há menos conectividade entre o cliente e o servidor. O servidor pode até mesmo recorrer à ofuscação de seus próprios URLs usando base64 ou codificações semelhantes se quiser enfatizar aos clientes que eles não devem "adivinhar" qual é o formato do URL ou inferir o significado dos URLs a partir de seu formato.
No exemplo anterior, usei a notação URI relativa de referências, por exemplo
/people/98765
. Talvez o cliente ficasse um pouco mais confortável (embora o autor não tenha sido muito útil na formatação deste post) se eu expressasse o URI de forma absoluta, por exemplo. pets.org/people/98765... Os clientes só precisam saber as regras de URI padrão definidas nas especificações IETF para converter esses URIs de um formulário para outro, portanto, escolher um formulário específico para URI não é tão importante quanto você possa imaginar. Compare essa situação com a conversão acima de chave estrangeira em URL, que exigia conhecimento específico da API da loja de animais. URLs relativos são um pouco mais convenientes para implementadores de servidor, conforme discutido abaixo, mas URLs absolutos são talvez mais convenientes para a maioria dos clientes. É provavelmente por isso que as APIs do Google Drive e GitHub usam URLs absolutos.
Resumindo, o uso de links em vez de chaves estrangeiras para expressar relacionamentos entre APIs reduz a quantidade de informações que um cliente precisa saber para interagir com a API e também reduz a quantidade de conectividade que pode ocorrer entre clientes e servidores.
Rochas subaquáticas
Aqui estão algumas coisas a serem consideradas antes de passar a usar links.
Muitas implementações de API foram fornecidas com proxies reversos para segurança, balanceamento de carga e muito mais. Alguns proxies gostam de reescrever URLs. Quando a API usa chaves estrangeiras para representar relacionamentos, a única URL que precisa ser reescrita no proxy é a URL de solicitação principal. Em HTTP, esse URL é dividido entre a barra de endereço (a primeira linha do cabeçalho) e o cabeçalho do host.
Uma API que usa links para expressar relacionamentos terá outros URLs nos cabeçalhos e no corpo da solicitação e da resposta, e esses URLs também precisarão ser reescritos. Existem várias maneiras de lidar com isso:
- URL . URL, .
- , . , , , -, , .
- . URL; , URL , -. URL, , , , , . - (, URL, «» «»), . , URL, , URL , , , URL .
URLs relativos sem barras iniciais também são mais difíceis para os clientes usarem porque eles precisam trabalhar com uma biblioteca padronizada em vez de apenas concatenação de strings para lidar com esses URLs e compreender e armazenar cuidadosamente o URL base.
Usar uma biblioteca padronizada para lidar com URLs é uma boa prática para clientes, mas muitos clientes não.
Ao usar links, você também pode precisar verificar a versão da API. Em muitas APIs, é comum colocar números de versão no URL, como este:
/v1/pets/12345
/v2/pets/12345
/v1/people/98765
/v2/people/98765
Este é o tipo de controle de versão, em que os dados de um determinado recurso podem ser exibidos simultaneamente em mais de um “formato” - não se trata de versões que se substituem ao longo do tempo conforme são posteriormente editadas.
Essa situação é muito semelhante à capacidade de visualizar o mesmo documento em vários idiomas naturais, para os quais existe um padrão da web; É uma pena que não exista tal padrão para versões. Ao atribuir a cada versão seu próprio URL, você atualiza cada versão para um recurso da web totalmente funcional. Não há nada de errado com "URLs com versão" desse tipo, mas eles não são adequados para expressar links. Se o cliente solicitar Lassie no formato versão 2, isso não significa que ele também deseja receber no formato 2 informações sobre Joe, o proprietário de Lassie, portanto o servidor não pode escolher qual número de versão incluir no link.
Talvez o formato 2 para descrever os proprietários nem mesmo seja fornecido. Também não há nenhum ponto conceitual em usar uma versão específica da URL em seus links - afinal, Lassie não pertence a uma versão específica de Joe, mas ao próprio Joe. Portanto, mesmo se você fornecer um URL no formato / v1 / people / 98765 e, assim, identificar uma versão específica de Joe, você também deve fornecer o URL / people / 98765 para identificar o próprio Joe, e é a segunda opção que você usa em links. Outra opção é definir apenas a URL / people / 98765 e permitir que os clientes selecionem uma versão específica, incluindo o cabeçalho da solicitação para isso. Não existe um padrão para este cabeçalho, mas se você chamá-lo de Accept-Version, ele funciona bem com a nomenclatura de cabeçalhos padrão.Pessoalmente, prefiro usar um cabeçalho para controle de versão e evitar usar números de versão na URL. mas URLs com números de versão são populares e frequentemente implemento o título também. e "URLs versionados", uma vez que é mais fácil implementar ambos do que argumentar qual é melhor. Você pode ler mais sobre o controle de versão da API neste artigo .
Você pode precisar documentar alguns padrões de url de qualquer maneira
Na maioria das APIs da web, um novo URL de recurso é alocado pelo servidor quando um novo recurso é criado usando o método POST. Se você usar este método para criar recursos e especificar relacionamentos usando links, não será necessário publicar um modelo para os URIs desses recursos. No entanto, algumas APIs permitem que o cliente controle a URL do novo recurso. Ao permitir que os clientes controlem as URLs de novos recursos, simplificamos muito muitos dos padrões de script da API para desenvolvedores de front-end e também suportamos scripts nos quais a API é usada para sincronizar o modelo de informações com uma fonte externa de informações. O HTTP fornece um método especial para esse propósito: PUT. PUT significa "criar um recurso neste URL se ainda não existir e, se existir, atualize-o."Se a sua API permite que os clientes criem novas entidades usando o método PUT, então você deve documentar as regras para a construção de novos URLs, talvez incluindo o padrão URI na especificação da API. Você também pode dar aos clientes controle parcial sobre o URL, incluindo um valor semelhante a uma chave primária no corpo ou nos cabeçalhos POST. Nesse caso, o padrão POST URI não é necessário per se, mas o cliente ainda terá que aprender o padrão URI para aproveitar ao máximo a previsibilidade do URI resultante.no entanto, o cliente ainda terá que aprender o padrão URI para aproveitar ao máximo a previsibilidade resultante do URI.no entanto, o cliente ainda terá que aprender o padrão URI para aproveitar ao máximo a previsibilidade resultante do URI.
Outro contexto em que é apropriado documentar padrões de URL é quando a API permite que os clientes codifiquem as solicitações. Nem toda API permite que você solicite seus recursos, mas isso pode ser muito útil para os clientes e, naturalmente, permite que os clientes codifiquem as solicitações por URL e recuperem os resultados usando um método GET. O exemplo a seguir mostra por quê.
No exemplo acima, incluímos o seguinte par nome / valor na visão de Joe:
"pets": "/pets?owner=/people/98765"
O cliente não precisa saber nada sobre sua estrutura para usar este URL, a não ser que foi escrito de acordo com as especificações padrão. Assim, o cliente pode obter uma lista dos animais de estimação de Joe a partir deste link, sem ter que aprender nenhuma linguagem de consulta. Também não há necessidade de documentar os formatos de seu URL na API - mas apenas se o cliente primeiro fizer uma solicitação GET para
/people/98765
... Se, além disso, a capacidade de fazer solicitações estiver documentada na API da loja de animais, o cliente pode redigir o mesmo URL de solicitação ou equivalente para recuperar os animais de estimação do proprietário de interesse, sem primeiro extrair o próprio proprietário - será o suficiente para saber o URI do proprietário. Talvez ainda mais importante, o cliente também pode gerar solicitações como as seguintes, o que de outra forma não seria possível: A especificação URI descreve, para esse propósito, uma parte do URL HTTP chamada de " componente de solicitação
/pets?owner=/people/98765&species=Dog
/pets?species=Dog&breed=Collie
"A parte do URL está depois do primeiro"? " ao primeiro “#.” O estilo de solicitação de URI que prefiro usar é sempre colocar solicitações específicas do cliente no componente de solicitação de URI, mas também é aceitável expressar solicitações de cliente na parte da URL chamada de “caminho . ”De qualquer forma, você precisa dizer aos clientes como esses URLs são construídos - na verdade, você está projetando e documentando a linguagem de solicitação específica para sua API. Claro, você também pode permitir que os clientes coloquem solicitações no corpo da mensagem, não no URL, e use o método POST em vez de GET. Limite prático de tamanho de url - acima de 4k bytes você é tentado todas as vezes - é recomendado oferecer suporte a POST para solicitações, mesmo que você já suporte GET.
Como as consultas são um recurso muito útil nas APIs e as linguagens de consulta não são fáceis de projetar e implementar, tecnologias como GraphQL surgiram . Nunca usei GraphQL, então não posso recomendá-lo, mas você pode considerá-lo como uma alternativa para implementar a capacidade de consulta em sua API. As ferramentas de solicitação de API, incluindo GraphQL, são mais usadas como um complemento para a API HTTP padrão para ler e escrever recursos, em vez de uma alternativa para HTTP.
E por falar nisso ... Qual é a melhor forma de escrever links em JSON?
JSON, ao contrário do HTML, não possui um mecanismo integrado para expressar links. Muitas pessoas têm sua própria maneira de entender como os links devem ser expressos em JSON, e algumas dessas opiniões foram publicadas em documentos mais ou menos oficiais, mas no momento não existem normas ratificadas por organizações respeitáveis que regulem isso. No exemplo acima, expressei links usando pares regulares de nome / valor escritos em JSON - eu prefiro esse estilo e, a propósito, esse estilo é usado no Google Drive e GitHub. Outro estilo que você provavelmente verá é este:
{"self": "/pets/12345",
"name": "Lassie",
"links": [
{"rel": "owner" ,
"href": "/people/98765"
}
]
}
Pessoalmente, não vejo para que esse estilo é bom, mas algumas de suas variações são bastante populares.
Há outro estilo de referência JSON de que gosto e é parecido com este:
{"self": "/pets/12345",
"name": "Lassie",
"owner": {"self": "/people/98765"}
}
A vantagem desse estilo é que ele fornece explicitamente:
"/people/98765"
é um URL, não apenas uma string. Aprendi esse padrão com RDF / JSON . Um dos motivos para dominar esse padrão é que você deve usá-lo de qualquer maneira, sempre que quiser exibir informações sobre um recurso aninhado em outro recurso, conforme mostrado no exemplo a seguir. Se você usar esse padrão em todo lugar, seu código obtém uma boa uniformidade:
{"self": "/pets?owner=/people/98765",
"type": "Collection",
"contents": [
{"self": "/pets/12345",
"name": "Lassie",
"owner": {"self": "/people/98765"}
}
]
}
Para obter mais informações sobre a melhor forma de usar JSON para representar dados, consulte JSON terrivelmente simples .
Finalmente, qual é a diferença entre um atributo e um relacionamento?
Eu acho que a maioria dos leitores concordará que JSON não possui um mecanismo integrado para expressar links, mas também há uma maneira de interpretar JSON que permite argumentar o contrário. Considere o seguinte JSON:
{"self": "/people/98765",
"shoeSize": 10
}
É geralmente aceito que
shoeSize
é um atributo, não um relacionamento, e 10 é um valor, não uma entidade. É verdade que não é menos lógico afirmar que a string '10 "é na verdade uma referência escrita em uma notação especial destinada a se referir a números, até o 11º inteiro, que é em si uma entidade. Se o 11º inteiro for uma entidade perfeitamente válida e a string
'10'
apenas apontar para ela, o par nome / valor é
'"shoeSize": 10'
conceitualmente uma referência, embora URIs não sejam usados aqui.
O mesmo pode ser dito para booleanos e strings, portanto, todos os pares nome / valor em JSON podem ser tratados como referências. Se você pensa dessa maneira sobre JSON, então é natural usar pares simples de nome / valor em JSON como referências a entidades que também podem ser apontadas usando uma URL.
De maneira mais geral, esse argumento é formulado como "não há diferença fundamental entre atributos e relacionamentos". Atributos são simplesmente relacionamentos entre uma entidade ou outra entidade abstrata ou concreta, como um número ou cor. Mas, historicamente, seu processamento foi tratado de maneira especial. Francamente, esta é uma versão bastante abstrata da percepção do mundo. Portanto, se você mostrar a alguém um gato preto e perguntar quantos objetos existem, a maioria das pessoas dirá que existe apenas um. Poucos diriam que vêem dois objetos - um gato e sua cor preta - e a relação entre eles.
Links são simplesmente melhores
APIs da Web que passam chaves de banco de dados em vez de apenas links são mais difíceis de aprender e também de usar para clientes. Além disso, as APIs do primeiro tipo ligam o cliente e o servidor mais intimamente, exigindo informações mais detalhadas como um "denominador comum", e todas essas informações precisam ser documentadas e lidas. A única vantagem das APIs do primeiro tipo é que elas são tão onipresentes que os programadores se sentem confortáveis com elas, sabem como criá-las e como consumi-las. Se você está procurando fornecer aos clientes APIs de alta qualidade que não requerem toneladas de documentação e maximizam a independência do cliente em relação ao servidor, considere fornecer links para suas APIs da web em vez de chaves de banco de dados.