Awesome
Guia para Projeto de API HTTP
Introdução
Este guia descreve uma série de boas práticas para o projeto de APIs HTTP+JSON, originalmente extraído de um trabalho na API da Plataforma Heroku.
Este guia inclui adições a essa API e serve de guia para novas APIs Internas no Heroku. Esperamos que seja de interesse de outros que projetam APIs que não são do Heroku.
Nosso objetivo aqui são consistência e foco nas regras de negócios evitando coisas supérfluas. Procuramos um jeito bom, consistente e bem documentado de projetar APIs, não necessariamente o método único/ideal.
Assumimos que você já esteja familiarizado com o básico de APIs HTTP+JSON APIs e não cobriremos todos os fundamentos disto nesse guia.
Agradecemos contribuições a esse guia.
Conteúdo
- Fundamentos
- Requisições
- Respostas
- Retorne o código de estado apropriado
- Prover recursos completos quando disponível
- Prover os (UU)IDs dos recursos
- Prover timestamps padrões
- Use horários UTC em formato ISO8601
- Aninhe as relações de chaves estrangeiras
- Gere erros estruturados
- Mostre o estado limite de requisições
- Manter o JSON minificado em todas as respostas
- Artefatos
- Traduções
Fundamentos
Separe as Responsabilidades
Mantenha as coisas simples quando for projetar separando as responsabilidades entre partes diferentes do ciclo de requisição e resposta. Mantendo regras simples aqui permite um foco maior em problemas maiores e mais complexos.
Requisições e respostas serão feitas para se dirigir a um recurso em particular ou coleção. Use o caminho para indicar a identidade, o corpo para transferir o conteúdo e os cabeçalhos para indicar meta dados. Parâmetros podem ser usado como um meio de passar informações de cabeçalhos em casos extremos, mas cabeçalhos são preferidos já que são mais flexiveis e podem transmitir informações mais diversas.
Exija Conexões Seguras
Exija conexões seguras com TLS para acessar a API, sem excessões. Não vale a pena ficar imaginando ou explicando quando se deve usar TLS e quando não. Simplesmente exija TLS para tudo.
O ideal é simplesmente rejeitar qualquer requisição não TLS não respondendo a requisição http ou na porta 80 para evitar troca de dados inseguros. Em ambientes que isto não é possível, responda com 403 Forbidden
.
Redirecionamentos são desencorajados uma vez que eles permitem um comportamento incorreto do cliente sem oferecer nenhum ganho. Clientes que dependem de redirecionamentos dobram o tráfego do servidor e fazem com que o TLS seja inútil uma vez que dados sensíveis já terão sido expostos na primeira requisição.
Exija Versionamento no Cabeçalho Accepts
Versionamento e a transição entre versões podem ser um dos aspectos mais desafiadores de projetar e manter uma API. Por isso, é melhor empregar mecanismos para facilitar isto desde o começo.
Para prevenir surpresas, indisponibilidade para o usuário, é melhor exigir que a versão seja especificada com todas as requisições. Versões padrões devem ser evitadas já que, no melhor dos casos, são muito difíceis de mudar no futuro.
É melhor enviar especificação da versão no cabeçalho, com outros meta dados, usando o cabeçalho Accept
com um content type personalizado, por exemplo:
Accept: application/vnd.heroku+json; version=3
Suporte ETags para Cacheamento
Inclua um cabeçalho ETag
em todas as respostas, identificando a versão específica do recurso retornado. Isto permite aos usuários cachear os recursos e usar requisições com este valor no cabeçalho If-None-Match
para determinar se o cache deve ser atualizado.
Forneça Request-Ids para Introspecção
Inclua um cabeçalho Request-Id
em cada resposta da API, populada com um valor UUID. Registrando esses valores no cliente, servidores e qualquer serviço adicional, oferece um mecanismo para rastrear, diagnosticar e depurar requisições.
Divida Respostas Longas Entre Requisições com Ranges
Respostas grandes devem ser quebradas entre múltiplas requisições usando o cabeçalho Range
para especificar quando mais dados estão disponíveis e como obter eles. Veja Heroku Platform API discussion of Ranges para os detalhes da requisição e dos cabeçalhos de respostas, códigos de estado, limites, organização e iteração.
Requisições
Aceite JSON serializado no corpo das requisições
Aceite JSON serializado no corpo das requisições PUT
/PATCH
/POST
além de ou em conjunto a dados form-encoded. Isto cria uma simetria com o corpo das requisições JSON serializado, p.e.:
$ curl -X POST https://service.com/apps \
-H "Content-Type: application/json" \
-d '{"name": "demoapp"}'
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "demoapp",
"owner": {
"email": "username@example.com",
"id": "01234567-89ab-cdef-0123-456789abcdef"
},
...
}
Use formatos de rotas consistentes
Nomes de recursos
Use a versão pluralizada de um nome de recurso a menos que o recurso em questão seja único no sistema (por exemplo, na maioria dos sistemas, um dado usuário deve ter somente uma conta). Isto permite consistência na forma que você se refere a um recurso em particular.
Ações
Prefira endpoint que não precisem de quaisquer ações especiais para recursos individuais. Em casos onde ações especiais são necessárias, coloque-as sobre um prefixo actions
padrão, para diferencia-los com clareza:
/resources/:resource/actions/:action
p.e.
/runs/{run_id}/actions/stop
Rotas e atributos em letras minúsculas
Use rotas com letras minúsculas e separadas por hífen, para que seja igual a nomes de domínio, p.e.:
service-api.com/users
service-api.com/app-setups
Também utilize letras minúsculas para os atributos, mas use sublinhado como separador pois assim nomes de atributos podem ser digitados sem aspas em JavaScript, p.e.:
service_class: "first"
Suporte referencia com atributos que não sejam ID por conveniência
Em alguns casos pode ser inconveniente para o usuário final oferecer IDs para identificar um recurso. Por exemplo, um usuário pode pensar no nome de Apps no Heroku, mas este app pode ser identificado por um UUID. Nestes casos você poderia aceitar ambos, p.e.:
$ curl https://service.com/apps/{app_id_or_name}
$ curl https://service.com/apps/97addcf0-c182
$ curl https://service.com/apps/www-prod
Não aceite somente nomes omitindo IDs.
Minimize a profundidade da rota
Em alguns modelos de dados com relacionamento de recursos pai/filho, as rotas podem acabar ficando muito profundas, p.e.:
/orgs/{org_id}/apps/{app_id}/dynos/{dyno_id}
Limite a profundidade excessiva preferindo localizar recursos na raiz da rota. Use aninhamento para indicar coleções. Por exemplo, para o caso acima em que um dyno pertence a um app, que pertence a uma organização:
/orgs/{org_id}
/orgs/{org_id}/apps
/apps/{app_id}
/apps/{app_id}/dynos
/dynos/{dyno_id}
Respostas
Retorne o código de estado apropriado
Retorne o código de estado HTTP adequado com cada resposta. Respostas com êxito devem retornar códigos de acordo com este guia:
200
: Requisição com êxito para um pedidoGET
,DELETE
ouPATCH
que se completou de forma síncrona, ou para um pedidoPUT
que atualizou um recurso existente de forma síncrona201
: Requisição com êxito para um pedidoPOST
que completou de forma síncrona, ou para um pedidoPUT
que completou de forma síncrona a criação de um novo recurso202
: Requisição aceita para um pedidoPOST
,PUT
,DELETE
, orPATCH
call que será processada de forma assíncrona206
: Requisição com êxito para um pedidoGET
, mas que retorna somente uma resposta parcial: veja acima sobre respostas longas
Preste atenção ao uso de códigos de erros para autenticação e autorização:
401 Unauthorized
: Requisição falhou por que o usuário não está autenticado403 Forbidden
: Requisição falhou por que o usuário não tem autorização para acessar o recurso especifico
Retorne códigos adequados para prover informações adicionais quando há erros:
422 Unprocessable Entity
: Sua requisição foi entendida, mas contém parâmetros inválidos429 Too Many Requests
: Você superou o limite de consumo, tente novamente mais tarde500 Internal Server Error
: Algo deu errado com o Servidor, confira o site do estado e/ou notifique o problema
Consulte a Especificação de códigos de respostas HTTP par um guia sobre código de estado para erros de usuário e servidor.
Prover recursos completos quando disponível
Disponibilize a representação completa do recurso (p.e. o objeto com todos os atributos) quando possível na resposta. Sempre disponibilize o recurso completo em respostas 200 e 201, incluindo requisições PUT
/PATCH
e DELETE
p.e.:
$ curl -X DELETE \
https://service.com/apps/1f9b/domains/0fd4
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
...
{
"created_at": "2012-01-01T12:00:00Z",
"hostname": "subdomain.example.com",
"id": "01234567-89ab-cdef-0123-456789abcdef",
"updated_at": "2012-01-01T12:00:00Z"
}
Respostas 202 não incluirão a representação total do recurso, p.e.:
$ curl -X DELETE \
https://service.com/apps/1f9b/dynos/05bd
HTTP/1.1 202 Accepted
Content-Type: application/json;charset=utf-8
...
{}
Prover os (UU)IDs dos recursos
Dê a cada recurso um atributo id
por padrão. Use UUIDs a menos que você tenham uma razão muito boa para não fazê-lo. Não use IDs que não são únicos globalmente entre instancias de serviços ou outros recursos no serviço, especialmente IDs com auto incremento.
Mostre os UUIDs em letras minúsculas no formato 8-4-4-4-12
, p.e.:
"id": "01234567-89ab-cdef-0123-456789abcdef"
Prover timestamps padrões
Disponibilize timestamps created_at
e updated_at
para recursos por padrão, p.e.:
{
// ...
"created_at": "2012-01-01T12:00:00Z",
"updated_at": "2012-01-01T13:00:00Z",
// ...
}
Estes timestamps talvez não façam sentido para alguns recursos, neste caso, podem ser omitidos.
Use horários UTC em formato ISO8601
Aceite e retorne horários somente em UTC. Mostre horários em formato ISO8601, p.e.:
"finished_at": "2012-01-01T12:00:00Z"
Aninhe as relações de chaves estrangeiras
Serialize as referencias a chaves estrangeiras com um objeto aninhado, p.e.:
{
"name": "service-production",
"owner": {
"id": "5d8201b0..."
},
// ...
}
Ao invés de p.e.:
{
"name": "service-production",
"owner_id": "5d8201b0...",
// ...
}
Este método possibilita mostrar mais informações sobre um recurso sem ter de mudar a estrutura de resposta ou introduzir mais campos de de resposta de alto nível p.e.:
{
"name": "service-production",
"owner": {
"id": "5d8201b0...",
"name": "Alice",
"email": "alice@heroku.com"
},
// ...
}
Gere erros estruturados
Gere corpos de respostas de erros estruturados e consistentes. Inclua um id
legível para máquinas, uma message
de erro legível para humanos e opcionalmente uma url
que aponte para mais informações sobre o erro e como resolver isto, p.e.:
HTTP/1.1 429 Too Many Requests
{
"id": "rate_limit",
"message": "Account reached its API rate limit.",
"url": "https://docs.service.com/rate-limits"
}
Documente seus formatos de erros e possíveis id
s de erros que os clientes possam encontrar.
Mostre o estado limite de requisições
Limite as requisições dos clientes para proteger o seu serviço e manter um serviço de alta qualidade para outros clientes. Você pode usar um algoritimo de token bucket para calcular o limite de respostas.
Retorne o número restante de requisições no cabeçalho de resposta RateLimit-Remaining
.
Manter o JSON minimizado em todas as respostas
Espaços extras adicionam tamanho extra as respostas, e muitos clientes que mostram os dados para humanos "embelezam" a saída em JSON. É melhor manter as respostas JSON mininizadas, p.e.:
{"beta":false,"email":"alice@heroku.com","id":"01234567-89ab-cdef-0123-456789abcdef","last_login":"2012-01-01T12:00:00Z","created_at":"2012-01-01T12:00:00Z","updated_at":"2012-01-01T12:00:00Z"}
Ao invés de p.e.:
{
"beta": false,
"email": "alice@heroku.com",
"id": "01234567-89ab-cdef-0123-456789abcdef",
"last_login": "2012-01-01T12:00:00Z",
"created_at": "2012-01-01T12:00:00Z",
"updated_at": "2012-01-01T12:00:00Z"
}
Você pode, opcionalmente, prover um jeito para os clientes de receber uma respostas mais verbosa, tanto via um parâmetro query (p.e. ?pretty=true
)
como via um parâmetro do cabeçalho Accept
(e.g.
Accept: application/vnd.heroku+json; version=3; indent=4;
).
Artefatos
Prover um esquema JSON processável
Disponibilize um esquema processável para especificar sua API. Use
prmd para gerenciar o seu esquema e assegure que está validado com prmd verify
.
Prover documentação para leitura
Disponibilize uma documentação para que os desenvolvedores possam entender a sua API.
Se você criar um esquema com prmd, como descrito acima, você pode facilmente gerar documentação em Markdown para todos os endpoints com
prmd doc`.
Em adição aos detalhes do endpoint, disponibilize um resumo da API com informações sobre:
- Autenticação, incluindo adquirir e usar tokens de autenticação.
- Estabilidade e versionamento da API, incluindo como selecionar a versão desejada da API.
- Cabeçalhos padrões de requisições e respostas.
- Formato de resposta de erro.
- Exemplos de utilização da API com clientes em diferentes linguagens.
Prover exemplos executáveis
Disponibilize exemplos executáveis que os usuários possam utilizar direto em seus terminais para ver como a API trabalha. Sempre que possível, estes exemplos devem ser palavra por palavras, para minimizar o trabalho que um usuário precisa fazer para testar a API, p.e.:
$ export TOKEN=... # acquire from dashboard
$ curl -is https://$TOKEN@service.com/users
Se você usa prmd para gerar documentação em Markdown, você terá exemplos para cada endpoint inclusos.
Descreva a estabilidade
Descreva a estabilidade da sua API ou seus vários endpoints de acordo com a maturidade e estabilidade, p.e. com sinalização de protótipo/desenvolvimento/produção.
Veja a politica de compatibilidade da API do Heroku para um exemplo de estabilidade e gerenciamento de mudanças.
Uma vez que sua API está declarada como estável e pronta para produção, não faça mudanças incompatíveis com a mesma versão de API. Se você precisar mudanças incompatíveis, crie uma nova API com numero de versão superior.
Traduções
- Versão em Português (a partir de fba98f08b5), por @Gutem
- Versão em Espanhol (a partir de 2a74f45), por @jmnavarro
- Versão em Coreano (a partir de f38dba6), por @yoondo
- Versão em Chines Simplificado (a partir de 337c4a0), por @ZhangBohan
- Versão em Chinês Tradicional (a partir de 232f8dc), por @kcyeu
- Versão em Turco (a partir de c03842f), por @hkulekci
- Versão original em Inglês, por @interagent