Melhores práticas para design de API REST

Neste artigo, analisaremos como projetar APIs REST para serem fáceis de entender para qualquer pessoa que as consuma, à prova de futuro e segura e rápida, uma vez que servem dados para clientes que podem ser confidenciais.

As APIs REST são um dos tipos mais comuns de serviços web disponíveis hoje. Eles permitem que vários clientes, incluindo aplicativos de navegador, se comuniquem com um servidor através da API REST.

 

Portanto, é muito importante projetar APIs REST corretamente para que não tenhamos problemas na estrada. Temos que levar em conta segurança, desempenho e facilidade de uso para os consumidores de API. 

 

Caso contrário, criamos problemas para clientes que usam nossas APIs, o que não é agradável e prejudica as pessoas de usar nossa API. Se não seguirmos convenções comumente aceitas, então confundimos os mantenedores da API e os clientes que as usam, já que é diferente do que todos esperam.

 

Neste artigo, analisaremos como projetar APIs REST para serem fáceis de entender para qualquer pessoa que as consuma, à prova de futuro e segura e rápida, uma vez que servem dados para clientes que podem ser confidenciais.

 

Como existem várias maneiras de um aplicativo em rede quebrar, devemos garantir que quaisquer APIs rest lidem com erros graciosamente usando códigos HTTP padrão que ajudam os consumidores a lidar com o problema.

 

Aceite e responda com JSON

 

As APIs REST devem aceitar JSON para solicitar carga útil e também enviar respostas à JSON. JSON é o padrão para a transferência de dados. Quase todas as tecnologias em rede podem usá-la: o JavaScript tem métodos incorporados para codificar e decodificar o JSON através da API Fetch ou de outro cliente HTTP. As tecnologias do lado do servidor têm bibliotecas que podem decodificar json sem fazer muito trabalho.

 

Há outras maneiras de transferir dados. O XML não é amplamente suportado por frameworks sem transformar os dados nós mesmos em algo que pode ser usado, e isso geralmente é JSON. Não podemos manipular esses dados tão facilmente do lado do cliente, especialmente em navegadores. Acaba sendo um monte de trabalho extra apenas para fazer a transferência normal de dados.

 

Os dados do formulário são bons para o envio de dados, especialmente se quisermos enviar arquivos. Mas para texto e números, não precisamos de dados de formulário para transferir esses, já que — com a maioria dos frameworks — podemos transferir o JSON apenas obtendo os dados deles diretamente do lado do cliente. É de longe o mais simples de fazer isso.

 

Para garantir que, quando nosso aplicativo REST API responda com json que os clientes o interpretem como tal, devemos definir Content-Type no cabeçalho de resposta ao application/json após a solicitação ser feita. Muitas estruturas de aplicativos do lado do servidor definem o cabeçalho de resposta automaticamente. Alguns clientes HTTP olham para o cabeçalho de resposta Content-Type e analisam os dados de acordo com esse formato.

 

A única exceção é se estamos tentando enviar e receber arquivos entre cliente e servidor. Em seguida, precisamos lidar com as respostas do arquivo e enviar dados de formulário de cliente para servidor. Mas isso é um tema para outra hora. 

 

Devemos também garantir que nossos pontos finais retornem json como resposta. Muitas estruturas do lado do servidor têm isso como um recurso incorporado.

 

Vamos dar uma olhada em uma API de exemplo que aceita cargas JSON. Este exemplo usará a estrutura de back-end do Express para .js Node. Podemos usar o middleware body-parser para analisar o corpo de solicitação JSON, e então podemos chamar o método res.json com o objeto que queremos retornar como a resposta JSON da seguinte forma:

 

const express = require('express');

const bodyParser = require('body-parser');

 

const app = express();

 

app.use(bodyParser.json());

 

app.post('/', (req, res) = {

  res.json(req.body);

});

 

app.listen(3000, () = console.log('server started'));

bodyParser.json() analisa a sequência do corpo de solicitação JSON em um objeto JavaScript e, em seguida, atribui-o ao objeto req.body

 

Defina o cabeçalho Content-Type na resposta ao application/json; charset=utf-8 sem alterações. O método acima se aplica à maioria das outras estruturas back-end.

 

Use substantivos em vez de verbos em caminhos de ponto final

 

Não devemos usar verbos em nossos caminhos de ponto final. Em vez disso, devemos usar os substantivos que representam a entidade que o ponto final que estamos recuperando ou manipulando como o nome do caminho.

 

Isso porque nosso método de solicitação HTTP já tem o verbo. Ter verbos em nossos caminhos de ponto final da API não é útil e faz com que seja desnecessariamente longo, já que não transmite nenhuma informação nova. Os verbos escolhidos podem variar por capricho do desenvolvedor. Por exemplo, alguns como 'get' e outros como 'recuperar', então é melhor deixar o verbo HTTP GET nos dizer o que e ponto final faz.

 

A ação deve ser indicada pelo método de solicitação HTTP que estamos fazendo. Os métodos mais comuns incluem GET, POST, PUT e DELETE.

 

GET recupera recursos. O POST envia novos dados ao servidor. PUT atualiza os dados existentes. O DELETE remove dados. O mapa de verbos para as operações crud.

 

Com os dois princípios que discutimos acima em mente, devemos criar rotas como GET /articles/ para obter artigos de notícias. Da mesma forma, /articles/ é para adicionar um novo artigo , PUT /articles/:id é para atualizar o artigo com o iddado . DELETE /articles/:id é para excluir um artigo existente com o ID dado.

 

/articles representam um recurso de API REST. Por exemplo, podemos usar o Express para adicionar os seguintes pontos finais para manipular artigos da seguinte forma:

 

const express = require('express');

const bodyParser = require('body-parser');

 

const app = express();

 

app.use(bodyParser.json());

 

app.get('/articles', (req, res) = {

  const articles = [];

  // code to retrieve an article...

  res.json(articles);

});

 

app.post('/articles', (req, res) = {

  // code to add a new article...

  res.json(req.body);

});

 

app.put('/articles/:id', (req, res) = {

  const { id } = req.params;

  // code to update an article...

  res.json(req.body);

});

 

app.delete('/articles/:id', (req, res) = {

  const { id } = req.params;

  // code to delete an article...

  res.json({ deleted: id });

});

 

app.listen(3000, () = console.log('server started'));

No código acima, definimos os pontos finais para manipular artigos. Como podemos ver, os nomes do caminho não têm verbos neles. Tudo o que temos são substantivos. Os verbos estão nos verbos HTTP.

 

Os pontos finais POST, PUT e DELETE tomam json como o órgão de solicitação, e todos eles retornam JSON como resposta, incluindo o ponto final GET.

 

Coleções de nomes com substantivos plurais

 

Devemos nomear coleções com substantivos plurais. Não é sempre que queremos apenas obter um único item, então devemos ser consistentes com nossa nomeação, devemos usar substantivos plurais.

 

Usamos plurais para sermos consistentes com o que está em nossas bases de dados. As tabelas geralmente têm mais de uma entrada e são nomeadas para refletir isso, por isso, para sermos consistentes com elas, devemos usar a mesma linguagem que a tabela que a API acessa.

 

Com o ponto final /articles temos a forma plural para todos os pontos finais, por isso não precisamos mudá-la para ser plural.

 

Recursos de aninhamento para objetos hierárquicos

 

O caminho dos pontos finais que lidam com recursos aninhados deve ser feito anexando o recurso aninhado como o nome do caminho que vem após o recurso pai.

 

Temos que garantir que ele certifique-se de que o que consideramos um recurso aninhado corresponda ao que temos em nossas tabelas de banco de dados. Caso contrário, será confuso.

 

Por exemplo, se queremos um ponto final para obter os comentários de um artigo de notícias, devemos anexar o caminho /comments até o fim do caminho /articles Isso assumindo que temos comments como uma criança de um article em nosso banco de dados.

 

Por exemplo, podemos fazer isso com o seguinte código no Express:

 

const express = require('express');

const bodyParser = require('body-parser');

 

const app = express();

 

app.use(bodyParser.json());

 

app.get('/articles/:articleId/comments', (req, res) = {

  const { articleId } = req.params;

  const comments = [];

  // code to get comments by articleId

  res.json(comments);

});

 

 

app.listen(3000, () = console.log('server started'));

No código acima, podemos usar o método GET no caminho '/articles/:articleId/comments'. Recebemos comments sobre o artigo identificado pelo articleId e depois devolvemos na resposta. Adicionamos 'comments' após o segmento de caminho '/articles/:articleId' para indicar que é um recurso infantil de /articles.

 

Isso faz sentido, pois os comments são objetos infantis dos artigos, assumindo que cada artigo tem seus articlescomentários. Caso contrário, é confuso para o usuário, uma vez que essa estrutura é geralmente aceita para acessar objetos infantis. O mesmo princípio também se aplica aos pontos finais POST, PUT e DELETE. Todos eles podem usar o mesmo tipo de estrutura de aninhamento para os nomes do caminho.

 

Manuseie erros graciosamente e devolva códigos de erro padrão

 

Para eliminar a confusão dos usuários de API quando ocorre um erro, devemos lidar com erros graciosamente e retornar códigos de resposta HTTP que indicam que tipo de erro ocorreu. Isso dá aos mantenedores da API informações suficientes para entender o problema que ocorreu. Não queremos que erros derrubem nosso sistema, então podemos deixá-los descontrolados, o que significa que o consumidor de API tem que lidar com eles.

 

Os códigos de status HTTP de erro comum incluem:

 

400 Má Solicitação – Isso significa que a entrada do lado do cliente falha na validação.

401 Não autorizado – Isso significa que o usuário não está autorizado a acessar um recurso. Geralmente retorna quando o usuário não é autenticado.

403 Proibido – Isso significa que o usuário é autenticado, mas não tem permissão para acessar um recurso.

404 Não Encontrado – Isso indica que um recurso não é encontrado.

500 Erro interno do servidor – Este é um erro genérico do servidor. Provavelmente não deveria ser jogado explicitamente.

502 Bad Gateway – Isso indica uma resposta inválida de um servidor upstream.

503 Serviço Indisponível – Isso indica que algo inesperado aconteceu no lado do servidor (pode ser qualquer coisa como sobrecarga de servidor, algumas partes do sistema falharam, etc.).

Deveríamos estar lançando erros que correspondem ao problema que nosso aplicativo encontrou. Por exemplo, se quisermos rejeitar os dados da carga útil da solicitação, então devemos retornar uma resposta de 400 da seguinte forma em uma API Expressa:

 

const express = require('express');

const bodyParser = require('body-parser');

 

const app = express();

 

// existing users

const users = [

  { email: 'abc@foo.com' }

]

 

app.use(bodyParser.json());

 

app.post('/users', (req, res) = {

  const { email } = req.body;

  const userExists = users.find(u = u.email === email);

  if (userExists) {

    return res.status(400).json({ error: 'User already exists' })

  }

  res.json(req.body);

});

 

 

app.listen(3000, () = console.log('server started'));

No código acima, temos uma lista de usuários existentes no array de users com o e-mail dado.

 

Então, se tentarmos enviar a carga útil com o valor de email que já existe nos usersteremos um código de status de resposta de 400 com uma mensagem 'User already exists' para que os usuários saibam que o usuário já existe. Com essas informações, o usuário pode corrigir a ação alterando o e-mail para algo que não existe.

 

Os códigos de erro precisam ter mensagens acompanhadas com eles para que os mantenedores tenham informações suficientes para solucionar o problema, mas os atacantes não podem usar o conteúdo de erro para carregar nossos ataques, como roubar informações ou derrubar o sistema.

 

Sempre que nossa API não for concluída com sucesso, devemos falhar graciosamente enviando um erro com informações para ajudar os usuários a fazer ações corretivas.

 

Permitir filtragem, classificação e paginação

 

Os bancos de dados por trás de uma API REST podem ficar muito grandes. Às vezes, há tantos dados que não devem ser devolvidos de uma vez porque é muito lento ou vai derrubar nossos sistemas. Portanto, precisamos de maneiras de filtrar itens.

 

Também precisamos de maneiras de pagiar dados para que só retornemos alguns resultados de cada vez. Não queremos amarrar recursos por muito tempo tentando obter todos os dados solicitados de uma só vez.

 

Filtragem e paginação aumentam o desempenho reduzindo o uso de recursos do servidor. À medida que mais dados se acumulam no banco de dados, mais importantes esses recursos se tornam.

 

Aqui está um pequeno exemplo onde uma API pode aceitar uma sequência de consulta com vários parâmetros de consulta para nos deixar filtrar itens por seus campos:

 

const express = require('express');

const bodyParser = require('body-parser');

 

const app = express();

 

// employees data in a database

const employees = [

  { firstName: 'Jane', lastName: 'Smith', age: 20 },

  //...

  { firstName: 'John', lastName: 'Smith', age: 30 },

  { firstName: 'Mary', lastName: 'Green', age: 50 },

]

 

app.use(bodyParser.json());

 

app.get('/employees', (req, res) = {

  const { firstName, lastName, age } = req.query;

  let results = [...employees];

  if (firstName) {

    results = results.filter(r = r.firstName === firstName);

  }

 

  if (lastName) {

    results = results.filter(r = r.lastName === lastName);

  }

 

  if (age) {

    results = results.filter(r = +r.age === +age);

  }

  res.json(results);

});

 

app.listen(3000, () = console.log('server started'));

No código acima, temos a variável req.query para obter os parâmetros de consulta. Em seguida, extraimos os valores da propriedade desestruturando os parâmetros de consulta individual em variáveis usando a sintaxe desestruturadora JavaScript. Finalmente, executamos filter com cada valor do parâmetro de consulta para localizar os itens que queremos devolver.

 

Uma vez feito isso, retornamos os results como resposta. Portanto, quando fazemos uma solicitação GET para o seguinte caminho com a sequência de consulta:

 

/employees?lastName=Smithage=30

 

Nós temos:

 

[

    {

        "firstName": "John",

        "lastName": "Smith",

        "age": 30

    }

]

como a resposta retornada desde que filtramos por lastName e age.

 

Da mesma forma, podemos aceitar o parâmetro de consulta de page e retornar um grupo de entradas na posição de (page - 1) * 20 para page * 20. 

 

Também podemos especificar os campos para classificar na sequência de consulta. Por exemplo, podemos obter o parâmetro de uma sequência de consulta com os campos para os queremos classificar os dados. Então podemos classificá-los por esses campos individuais.

 

Por exemplo, podemos querer extrair a sequência de consulta de uma URL como:

 

http://example.com/articles?sort=+author,-datepublished

 

Onde + significa ascendente e - significa descer. Assim, classificamos pelo nome do autor em ordem alfabética e datepublished mais recente para menos recente.

 

Manter boas práticas de segurança

 

A maioria da comunicação entre cliente e servidor deve ser privada, uma vez que muitas vezes enviamos e recebemos informações privadas. Portanto, o uso do SSL/TLS para segurança é uma obrigação.

 

Um certificado SSL não é muito difícil de carregar em um servidor e o custo é gratuito ou muito baixo. Não há razão para não fazer nossas APIs rest se comunicarem por canais seguros em vez de em tempo aberto.

 

As pessoas não devem ter acesso a mais informações que solicitaram. Por exemplo, um usuário normal não deve ser capaz de acessar informações de outro usuário. Eles também não devem ser capazes de acessar dados de administradores.

 

Para impor o princípio do menor privilégio, precisamos adicionar verificações de função ou para uma única função, ou ter funções mais granulares para cada usuário.

 

Se optarmos por agrupar os usuários em algumas funções, então as funções devem ter as permissões que cobrem tudo o que precisam e não mais. Se tivermos permissões mais granulares para cada recurso a que os usuários têm acesso, então temos que garantir que os administradores possam adicionar e remover esses recursos de cada usuário de acordo. Além disso, precisamos adicionar algumas funções predefinidas que podem ser aplicadas a usuários de grupo para que não tenhamos que fazer isso manualmente para cada usuário.

 

Dados de cache para melhorar o desempenho

 

Podemos adicionar cache para retornar dados do cache de memória local em vez de consultar o banco de dados para obter os dados toda vez que quisermos recuperar alguns dados que os usuários solicitam. O bom do cache é que os usuários podem obter dados mais rapidamente. No entanto, os dados que os usuários obtêm podem estar desatualizados. Isso também pode levar a problemas ao depurar em ambientes de produção quando algo dá errado à medida que continuamos vendo dados antigos.

 

Existem muitos tipos de soluções de cache como Redis,cache na memória, e muito mais. Podemos mudar a forma como os dados são armazenados em cache à medida que nossas necessidades mudam.

 

Por exemplo, o Express tem o middleware apicache para adicionar cache ao nosso aplicativo sem muita configuração. Podemos adicionar um simples cache de memória em nosso servidor assim:

 

const express = require('express');

const bodyParser = require('body-parser');

const apicache = require('apicache');

const app = express();

let cache = apicache.middleware;

app.use(cache('5 minutes'));

 

// employees data in a database

const employees = [

  { firstName: 'Jane', lastName: 'Smith', age: 20 },

  //...

  { firstName: 'John', lastName: 'Smith', age: 30 },

  { firstName: 'Mary', lastName: 'Green', age: 50 },

]

 

app.use(bodyParser.json());

 

app.get('/employees', (req, res) = {

  res.json(employees);

});

 

app.listen(3000, () = console.log('server started'));

O código acima apenas faz referência ao middleware apicache com apicache.middleware e então temos:

 

app.use(cache('5 minutes'))

 

para aplicar o cache em todo o aplicativo. Armazenamos os resultados por cinco minutos, por exemplo. Podemos ajustar isso para nossas necessidades

 

Versão de nossas APIs

 

Devemos ter versões diferentes da API se fizermos alterações neles que possam quebrar os clientes. A versão pode ser feita de acordo com a versão semântica (por exemplo, 2.0.6 para indicar a versão principal 2 e o sexto patch) como a maioria dos aplicativos fazem hoje em dia.

 

Dessa forma, podemos gradualmente eliminar os pontos finais antigos em vez de forçar todos a se mudarem para a nova API ao mesmo tempo. O ponto final v1 pode permanecer ativo para pessoas que não querem mudar, enquanto o v2, com suas novas características brilhantes, pode servir aqueles que estão prontos para atualizar. Isso é especialmente importante se nossa API for pública. Devemos version-los para que não queremos aplicativos de terceiros que usem nossas APIs.

 

A versão geralmente é feita com /v1/, /v2/, etc. adicionado no início do caminho da API.

 

Por exemplo, podemos fazer isso com o Express da seguinte forma:

 

const express = require('express');

const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

 

app.get('/v1/employees', (req, res) = {

  const employees = [];

  // code to get employees

  res.json(employees);

});

 

app.get('/v2/employees', (req, res) = {

  const employees = [];

  // different code to get employees

  res.json(employees);

});

 

app.listen(3000, () = console.log('server started'));

Apenas adicionamos o número da versão ao início do caminho de URL do ponto final para version-los.

 

Conclusão

 

A vantagem mais importante para projetar APIs de ressarem de alta qualidade é ter consistência seguindo padrões e convenções da Web. Os códigos de status JSON, SSL/TLS e HTTP são todos blocos de construção padrão da web moderna.

 

O desempenho também é uma consideração importante. Podemos aumentá-lo não devolvendo muitos dados ao mesmo tempo. Além disso, podemos usar cache para que não tenhamos que consultar dados o tempo todo.

 

Os caminhos dos pontos finais devem ser consistentes, usamos substantivos apenas uma vez que os métodos HTTP indicam a ação que queremos tomar. Os caminhos dos recursos aninhados devem vir após o caminho do recurso pai. Eles devem nos dizer o que estamos recebendo ou manipulando sem a necessidade de ler documentação extra para entender o que está fazendo.

 

 

O Avance Network é uma comunidade fácil de usar que fornece segurança de primeira e não requer muito conhecimento técnico. Com uma conta, você pode proteger sua comunicação e seus dispositivos. O Avance Network não mantém registros de seus dados; portanto, você pode ter certeza de que tudo o que sai do seu dispositivo chega ao outro lado sem inspeção.


Strong

5178 Блог сообщений

Комментарии