Introdução
API é uma solução bastante utilizada atualmente na construção de aplicações Web. É um "interfaceamento" no qual ocorre envio apenas de dados necessários, processamento e resposta e pode ser usada para separar funcionalidades de uma aplicação. Basicamente, ela permite que serviços possam se comunicar sem que um conheça o funcionamento interno do outro, o que simplifica bastante o desenvolvimento.
Ao criar uma aplicação Ruby on Rails, ela já possui toda a estrutura necessária para criar uma API. Existe inclusive uma opção para criar uma app que servirá somente como API, sem expor páginas HTML para o usuário final, por exemplo. Todos esses detalhes estão documentados no Rails Guides. Se preferir, também existe uma versão traduzida para o português aqui
Em resumo: - API é um conjunto de recursos, sendo que cada um realiza uma ação, como criar um item, listar itens, apagar um item. Traduzindo para nossa visão dentro de uma aplicação Ruby on Rails: cada endpoint é uma rota que direciona uma requisição para os controllers da nossa API. - Na criação de APIs, é possível reaproveitar rotas e controllers normalmente usados para as views, mas geralmente temos rotas e controllers separados para tratar os dados de forma independente. - É uma prática comum conter o versionamento dentro da URL de uma API, pois isso ajuda em futuras atualizações. - APIs são muito diferentes umas das outras e dependem muito das necessidades de cada cliente. Mas, em geral, são aplicações compostas por diversas pequenas funções.
Neste tutorial vamos construir uma API Ruby on Rails simples de lista de tarefas e usaremos Ruby 3.0 e a versão 6.1.1 do Rails. Apesar do Rails oferecer uma maneira de gerar uma aplicação especificamente para API, neste artigo utilizamos como base uma aplicação comum, mas os mesmos conceitos podem ser aplicados. Se você estiver criando uma aplicação que é dominantemente uma API, é possível usar o comando rails new my_api --api
.
Vamos começar mostrando como faríamos os primeiros testes e, para isso, gostamos de usar a gem rspec-rails
: documentação rspec-rails. Além disso, neste tutorial vamos utilizar uma abordagem com desenvolvimento guiado por testes ou TDD (Test-Driven Development).
Estrutura de um teste para API
O teste da API deve ser um teste do tipo Request no Rspec e ele segue a mesma estrutura de sempre: criação dos dados, execução do código e validação de expectativas. Mas as principais diferenças são:
- o código da API será executado por meio de chamadas HTTP, através dos métodos
get
,post
,delete
,patch
. - as expectativas vão validar o retorno que é atualizado automaticamente na variável
response
. Vamos testar sempre oresponse.status
e oresponse.body
.
O response.body
vai trazer toda resposta vinda via HTTP no formato string
. O que podemos fazer em casos de APIs JSON é transformar essa string
de volta em um Hash para testar mais detalhes como atributos específicos.
Para isso, o Rails já tem a Gem JSON incorporada: https://github.com/flori/json.
Mas antes de qualquer coisa, vamos criar um primeiro teste onde verificamos apenas o status de uma chamada:
require 'rails_helper'
describe 'TasksController', type: :request do
context 'index' do
it 'should return status ok' do
get api_v1_tasks_path
expect(response).to have_http_status(200)
end
end
end
Neste teste queremos apenas criar uma rota index
onde serão listadas todas as tarefas cadastradas, mas, neste momento, vamos apenas garantir que ela retornará status 200
.
Como criar rotas para cada versão
Note como em nosso teste usamos get '/api/v1/tasks'
. Fizemos isso porque vamos separar as rotas de nossa API em versões. Essa prática é importante para que fiquem claramente separadas as lógicas aplicadas em diferentes versões da aplicação e, principalmente, evitar que interfaces API que consomem versões antigas quebrem devido às mudanças. Para que possamos ter uma rota como /api/v1/tasks
, podemos utilizar duas estratégias: namespaces ou scopes.
Namespaces
Rails.application.routes.draw do
namespace 'api' do
namespace 'v1' do
resources :tasks, only: [:index]
end
end
end
Scopes
Rails.application.routes.draw do
scope '/api/v1/' do
resources :tasks, only: [:index]
end
end
Tanto Namespaces quanto Scopes cumprem o papel de configurar a URL exatamente do jeito que queremos. No entanto, como pretendemos criar controllers diferentes para versões de API, utilizaremos Namespaces por permitir o uso de módulos para separar as classes.
Como criar o Controller
Com a implementação que fizemos no routes.rb
, o controller esperado agora é Api::V1::TasksController
. Para refletir isso vamos criar uma nova estrutura de pastas para os controllers da API /app/controllers/api/v1/
. Dentro dessa pasta coloque seu novo tasks_controller.rb
. Seu novo controller deverá fazer parte de um module
do Ruby. Você pode ler mais aqui. A implementação poderia ficar assim:
module Api
module V1
class TasksController < Api::V1::ApiController
end
end
end
Mas aqui nós vamos seguir o seguinte modelo:
class Api::V1::TasksController < Api::V1::ApiController
end
Repare que TasksController
herda de ApiController
, que teria o mesmo papel de um ApplicationController
, ou seja, nos dar mais controle sobre o código. A autenticação pode ser um exemplo: podemos ter uma API 100% pública, mas nossa aplicação poderia requerer autenticação de usuários em diferentes rotas.
Então, crie a classe Api::V1::ApiController
dentro da pasta /app/controllers/api/v1/
e coloque o conteúdo:
class Api::V1::ApiController < ActionController::API
end
Actions e retorno
As actions podem seguir o mesmo modelo que seguimos no resto do desenvolvimento usual. No nosso exemplo, a action index
dentro do controller de tasks
poderia ficar assim:
def index
head 200
end
O método head
renderiza somente o head da resposta com o status indicado. Como não queremos renderizar uma view, mas planejamos que a aplicação envie junto com o status todas as tarefas cadastradas, vamos começar a implementar um código que gere uma resposta renderizando um JSON além do status.
def index
render json: {}, status: 200
end
O código acima fica incompleto, mas passa no teste. Agora vamos complementar nosso teste inicial para continuar a implementação, depois voltaremos neste ponto para finalizá-lo.
Para quem está se habituando com Ruby, encarar JSON como uma Hash é um bom atalho inicial, mas recomendamos ler e entender mais sobre o formato: https://www.w3schools.com/js/js_json_syntax.asp e https://jsonapi.org/.
Renderizando objetos em JSON
O teste
Vamos voltar ao nosso arquivo de testes e criar mais um cenário dentro do contexto de index
, dessa vez verificando a listagem de tarefas cadastradas no sistema.
require 'rails_helper'
describe 'TasksController', type: :request do
context 'index' do
it 'deve retornar status 200' do
get api_v1_tasks_path
expect(response).to have_http_status(200)
end
it 'deve retornar todas as tarefas' do
task = Task.create(title: 'Estudar Ruby',
description: 'Assistir aulas da Campus Code')
other_task = Task.create(title: 'Fazer lição de casa',
description: 'Ler apostilas e resolver exercícios')
get '/api/v1/tasks'
expect(response).to have_http_status(200)
expect(response.body).to include task.title
expect(response.body).to include task.description
expect(response.body).to include other_task.title
expect(response.body).to include other_task.description
end
end
end
Anteriormente, o teste verificava apenas se response
possuía o status HTTP 200
. Mas agora ele verifica se o body
da resposta inclui o título e a descrição de cada tarefa criada.
Para retornar um objeto em formato JSON dentro da nossa aplicação, vamos utilizar o método json
na index
em TasksController
:
def index
@tasks = Task.all
render json: @tasks, status: 200
end
Além do retorno do JSON, precisamos nos preocupar com o Status Code da resposta HTTP. Até agora não estávamos dando muita atenção para o status, pois o Rails resolve isso por conta própria. Mas agora queremos detalhar um pouco mais. No próprio método render
podemos adicionar um status e retornar um dos HTTP Status existentes, por exemplo: render json: @task, status: :ok
. No dia a dia, é mais comum testar e renderizar a mensagem de status em vez do código.
Lembre-se dos symbols disponíveis para a maioria dos status code comumente usados:
:ok
:not_found
:created
:no_content
:bad_request
:unauthorized
:forbidden
:precondition_failed
:unprocessable_entity
:internal_server_error
:service_unavailable
etc.
Apresentação de objeto
Caso queira apresentar um único objeto ou retornar que um objeto não existe, poderiam ser feitos testes como:
context 'show' do
it 'deve retornar uma tarefa' do
task = Task.create(title: 'Estudar Ruby',
description: 'Assistir aulas da Campus Code')
get api_v1_task_path(task)
expect(response).to have_http_status(200)
expect(response.body).to include task.title
expect(response.body).to include task.description
end
it 'deve retornar not found se tarefa indisponível' do
get api_v1_task_path(id:999)
expect(response).to have_http_status(:not_found)
end
end
No arquivo de rotas só seria necessário adicionar a action:
resources :tasks, only: [:index, :show]
E no controller o código poderia ficar mais ou menos assim:
def show
@task = Task.find(params[:id])
render status: 200, json: @task
rescue ActiveRecord::RecordNotFound
render status: 404, json: {}
end
Criação de objetos
Para criação de objetos poderia ser feito um teste como esse:
context 'post' do
it 'deve criar tarefa' do
task = {task: { title: 'Comprar leite',
description: "Comprar leite no mercadinho" } }
post api_v1_tasks_path, params: task
expect(response).to have_http_status(201)
expect(response.body).to include('Comprar leite')
expect(response.body).to include('Comprar leite no mercadinho')
expect(Task.last.title).to eq('Comprar leite')
end
end
No arquivo de rotas só seria necessário adicionar a action:
resources :tasks, only: [:index, :show, :create]
E no controller a action poderia ficar mais ou menos assim:
def create
@task = Task.create(task_params)
render status: 201, json: @task
end
private
def task_params
params.require(:task).permit(:title, :description)
end
Conclusão
Neste artigo mostramos os primeiros passos para começar a testar e criar uma API usando Ruby on Rails. A partir dessa base você já deve ser capaz de criar sua própria API e implementar o código para adequar às suas necessidades.
Se quiser se aprofundar um pouco mais e aprender a usar outras ferramentas como o Faraday ou VCR na sua aplicação, recomendamos a leitura dos nossos artigos abaixo. :)