Você já tem a sua API! Ela é REST, performa muito bem, e todos os seus aplicativos estão conversando com a mesma. Vem uma oportunidade de negócio de abrí-la para consumo de parceiros. Agora não basta só a técnica perfeita, você precisa de especificação, documentações, exemplos de uso e validadores para garantir que os contratos da sua API não sofram alterações drásticas, deixando seu cliente na mão.
Essa é a realidade da Loadsmart no momento da publicação desse post. Lá, optamos por seguir os mesmos passos do Spotify e utilizar o RAML para descrever as nossas APIs.
O RAML (RESTful API Modeling Language) é uma maneira simples de descrever/projetar suas APIs. Através de uma linguagem concisa, baseada em YAML, você é capaz de escrever especificações que podem ser usadas como documentação, ferramenta de build e de testes automatizados.
Ao invés de tentar cultivar a motivação para a utilização do RAML, vou descrever de forma prática os principais aspectos do padrão. Para tanto, já começo recomendando a instalação do API Workbench, um excelente plugin para o Atom que facilita (e muito) a escrita de documentos RAML.
Vamos voltar ao nosso "mini IMDB", exemplo utilizado em alguns posts aqui do blog. Os principais endpoints que temos são:
GET /movies: Listagem de filmes;POST /movies: Adição de um filme;GET /movies/{id}: Detalhes de um filme;PUT /movies/{id}: Atualização de um filme;DELETE /movies/{id}: Remoção de um filme.Podemos começar criando um arquivo api.raml com o seguinte conteúdo:
#%RAML 1.0
title: Movies APINo momento desse artigo, o RAML possui duas especificações ativas: a 0.8 e a 1.0. Dependendo
das ferramentas que você for utilizar posteriormente, talvez seja interessante optar pela especificação 0.8.
Nesse exemplo utilizaremos a 1.0.
Vamos continuar descrevendo a nossa API:
#%RAML 1.0
title: Movies API
version: v1
baseUri: https://api.movies.com/{version}
mediaType: application/jsonSetamos a versão da API como v1, atendendo pelo endereço https://api.movies.com/v1. Trabalharemos
com objetos JSON. Caso a API necessite trabalhar com mais de um tipo (XML, por exemplo),
é possível declará-lo no mediaType:
mediaType: [application/json, application/xml]Se você optar pela versão 0.8 do padrão, a palavra schemas aparecerá com certa frequência durante
a construção do documento. Com a 1.0, a comunidade depreciou o termo em prol da palavra-chave types, e é com
ela que descreveremos o tipo Movie:
(...)
types:
Movie:
properties:
id: string
title: string
description?: stringMovie possui três propriedades do tipo string: id, title e description. O ? em description?
indica que essa última é uma propriedade opcional (ao contrário de id e title que são obrigatórias).
O RAML suporta uma série de tipos built-in, para saber mais, leia sobre definição de tipos.
Agora que temos um tipo definido, podemos partir para a descrição dos nossos endpoints. Em RAML,
a semântica correta é chamarmos um endpoint de Resource. Vamos iniciar pelo /movies:
(...)
/movies:
description: A set of movies.
get:
description: Get a list of movies.
responses:
200:
body: Movie[]
post:
description: Add a new movie to the set.
body:
type: Movie
responses:
201:
description: Returns the new movie.
body: MovieComeçamos ao criar um bloco /movies. Nele, além de adicionarmos uma descrição através da propriedade
description, também deixamos explícito a possibilidade de enviarmos dois métodos: get e post.
Em get, podemos ter como resposta um status code 200 e uma lista de Movie (por isso o sufixo []).
Já em post, além de deixar claro que retornaremos o objeto recém criado, apontamos que a resposta
será um 201 com um único Movie. Note que nesse caso descrevemos que o post também possui um tipo,
através da palavra-chave body. Isso quer dizer que, ao fazermos POST /movies/, precisamos
passar um objeto que atenda as especificações do tipo Movie.
Caso a sua regra de negócio vá além da criação de um elemento, é possível descrevê-la também. Por exemplo, vamos imaginar que exista a necessidade de verificar se o filme já existe no banco de dados:
(...)
201:
description: Returns the new movie.
body: Movie
409:
description: The movie already exists in our database.
body:
properties:
error: stringNote que o padrão RAML é flexível. Nesse caso não criamos um tipo Error, apenas descrevemos o corpo
da resposta através das palavras-chave body e properties.
Vamos finalizar descrevendo os dois métodos restantes:
(...)
/{id}:
uriParameters:
id:
description: The Movie identifier.
type: string
get:
description: Gets a specific movie.
responses:
200:
description: Returns the specific movie.
body: Movie
put:
body:
type: Movie
description: Updates an already created movie.
responses:
200:
description: Returns the updated movie.
body: Movie
delete:
description: Deletes the movie.
responses:
204:
description: Confirms the deletion.Dentro de /movies, criamos um novo bloco chamado /{id}. Através da propriedade uriParameters deixamos
claro que id é na verdade uma string (no nosso caso, estamos usando um uuid). Além disso, descrevemos
os métodos get, put e delete para /movies/{id}, que não diferem tanto assim dos demais explicados
anteriormente.
No nosso exemplo utilizando Restless,
ao fazer um POST ou PUT com dados inválidos, retornamos um BadRequest. Podemos especificar esse
comportamento para o recurso POST movies/ e PUT movies/{id}.
Logo após o bloco types, vamos adicionar um novo bloco chamado traits:
(...)
mediaType: application/json
types:
(...)
traits:
dataValidation:
responses:
400:
description: A BadRequest happens when data validation fails.
body:
properties:
error: stringAtravés de traits somos capazes de escrever regras de uso que podem ser reaproveitadas por dados e recursos.
No exemplo acima, dizemos que recursos que utilizarem esse trait terão como resposta o status code 400.
Agora basta apontarmos nossos recursos ao trait de nome dataValidation:
(...)
/movies:
(...)
post:
is: [dataValidation]
(...)
/{id}:
(...)
put:
is: [dataValidation]
(...)Traits são extremamente úteis para descrever comportamentos que são comuns entre recursos (por exemplo, listagens que possuem paginação). Outro conceito similar é o Resource Types, no qual você pode ler mais sobre na documentação oficial.
Para finalizar nosso exemplo. Vamos supor que o acesso à API é limitado, e o usuário precisa ter uma conta para acessá-la. Para não reinventar a roda, vamos supor que optamos pelo padrão OAuth 2 para autenticação e autorização.
Abaixo da propriedade mediaType vamos criar um novo bloco chamado securitySchemes:
(...)
securitySchemes:
oauth_2_0:
description: We support OAuth 2.0 for authenticating all API requests.
type: OAuth 2.0
describedBy:
headers:
Authorization:
description: |
Used to send a valid OAuth 2 access token. Do not use
with the "access_token" query string parameter.
type: string
queryParameters:
access_token:
description: |
Used to send a valid OAuth 2 access token.
Do not use with the "Authorization" header.
type: string
responses:
401:
description: |
Bad or expired token. This can happen if the
user or Movie API revoked or expired an access
token. To fix, re-authenticate the user.
403:
description: |
Bad OAuth request (wrong consumer key, bad nonce,
expired timestamp...). Unfortunately,
re-authenticating the user won't help here.
settings:
authorizationUri: https://www.movies.com/1/oauth2/authorize
accessTokenUri: https://api.movies.com/1/oauth2/token
authorizationGrants: [ authorization_code, implicit ]Serei sincero com você, caro leitor, o código acima é uma receita de bolo para descrever
o securitySchemes do tipo OAuth 2. Nada de muito diferente do que a gente viu até aqui,
com exceção do uso do |, que nesse caso serve para fazer textos em bloco, e da
propriedade type com valor OAuth 2.0.
No nosso cenário, apenas temos intenção de proteger a escrita de dados na API. Para tanto,
Vamos adicionar a propriedade securedBy aos blocos post e put:
(...)
post:
is: [dataValidation]
securedBy: [oauth_2_0]
(...)
put:
is: [dataValidation]
securedBy: [oauth_2_0]
(...)Pronto! A especificação da nossa API está completa! Temos uma documentação forte, que pode ser lida
por humanos e máquinas. Para fins didáticos não utilizei recursos
interessantes como a propriedade example ou a ferramenta !include. Mas você pode ler sobre
eles na especificação do RAML 1.0.
Veja como ficou a versão final do nosso arquivo api.raml.
Através de uma linguagem clara fomos capazes de construir uma especificação legível para a nossa API. Com isso, outros desenvolvedores (ou até mesmo máquinas) serão capazes de entender como funciona cada endpoint. Com a adição de ferramentas como o Abao e raml2html, os resultados do uso do RAML podem ser surpreendentes, como no exemplo abaixo:
Até a próxima.