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 API
No 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/json
Setamos 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?: string
Movie
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: Movie
Começ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: string
Note 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: string
Atravé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.