Portuguese (Português) translation by Erick Patrick (you can also view the original English article)
Hoje em dia, é prática comum depender bastante de APIs (interfaces programáveis de aplicação). Não só grande serviços como Facebook e Twitter as usam—APIs são bem populares dada a gama de framework do lado do cliente, como React Angular e outras. Ruby on Rails segue a tendência e sua última versão apresenta um novo recurso que permite criar aplicações só API.
Inicialmente, essa funcionalidade era fornecida em uma gem separada chamada rails-api, mas desde Rails 5, é parte da base do framework. Esse curso, junto de ActionCable foi, provavelmente, o mais esperado e, hoje, iremos discuti-lo.
O artigo cobre como criar uma aplicação Rails só API e explica como estruturar as rotas e controladores, responder com JSON, adicionar serializadores e configurar CORS (Cross Origin Resource Sharing). Também aprenderemos sobre opções de segurança da API e protegê-la de abusos.
O código fonte do artigo está disponível no GitHub.
Criando uma Aplicação só API
Para começar, executemos o comando a seguir no terminal:
rails new RailsApiDemo --api
Ele criará uma aplicação Rails só API chamada RailsApiDemo
. Não esqueçamos que a opção --api
só funciona em Rails 5, então é bom certificar-se de tê-la instalada.
Abramos o Gemfile e vejamos que é bem menor que o normal: gem como coffer-rails
, turbolinks
e sass-rails
não estão presentes.
O arquivo config/application.rb contém uma linha nova:
config.api_only = true
Com isso o Rails carregará menos mediadores: por exemplo, não há suporte a cookies e sessões. Além disso, se tentarmos usar o gerador de esqueleto, visões e ativos não serão criados. Na verdade, se virmos a pasta views/layouts, notaremos que application.html.erb também não existe.
Outra diferença importante é que ApplicationController
herda de ActionController::API
, não de ActionController::Base
.
Fora isso, é uma aplicação Rails como qualquer outra já vista antes. Agora, adicionemos alguns modelos para termos algo com que trabalhar:
rails g model User name:string rails g model Post title:string body:text user:belongs_to rails db:migrate
Nada demais: um publicação com título e corpo, pertencendo a um usuário.
Garantamos que as associações apropriadas estão configurados e também provenhamos verificações simples:
models/user.rb
has_many :posts validates :name, presence: true
models/post.rb
belongs_to :user validates :title, presence: true validates :body, presence: true
Brilhante! O próximo passo é carregar alguns registros de exemplo nas tabelas recém criadas.
Carregando Dados Demo
A forma mais fácil é utilizando o arquivo seeds.rb dentro do direitório db. Contudo, somos preguiçosos (como outros programadores) e não queremos pensar em conteúdo exemplo. Assim, por que não usar da gem faker, que produz dados aleatórios dos mais vários tipos: nomes, emails, palavras hispsters, textos "lorem ipsum" e mais.
Gemfile
group :development do gem 'faker' end
Instalemos a gem:
bundle install
Agora, alteremos seeds.rb:
db/seeds.rb
5.times do user = User.create({name: Faker::Name.name}) user.posts.create({title: Faker::Book.title, body: Faker::Lorem.sentence}) end
Por último, carreguemos os dados:
rails db:seed
Respondendo Com JSON
Agora, claro, precisamos de algumas rotas e controladores para a API. É prática comum aninhar as rotas da API dentro do caminho api/
. Além disso, desenvolvedores proveem versionamento de APIs, como api/v1/
. Depois, se alguma mudança problemática for introduzida, podemos criar um novo espaço de nome (v2
) e um novo controlador.
Eis como as rotas podem parecer:
config/routes.rb
namespace 'api' do namespace 'v1' do resources :posts resources :users end end
Isso gera rotas assim:
api_v1_posts GET /api/v1/posts(.:format) api/v1/posts#index POST /api/v1/posts(.:format) api/v1/posts#create api_v1_post GET /api/v1/posts/:id(.:format) api/v1/posts#show
Podemos usar o método scope
ao invés de namespace
, mas, por padrão, ele buscará o UsersController
e PostsController
dentro da pasta controllers, não dentro de controllers/api/v1, então cuidado.
Criemos uma pasta api com a pasta v1 aninhada, dentro de controllers. Populemos com os controladores:
controllers/api/v1/users_controller.rb
module Api module V1 class UsersController < ApplicationController end end end
controllers/api/v1/posts_controller.rb
module Api module V1 class PostsController < ApplicationController end end end
Notemos que temos de, além de aninhar no caminho api/v1, mas a classe em si precisa ter o espaço de nome dentro dos módulos Api
e V1
.
A próxima questão é: como responder apropriadamente com da dados JSON formatados? Nesse artigo, tentaremos essas soluções: as gems jBuilder e active_model_serializers. Antes de seguir para a próxima seção, coloquemo-nos no Gemfile:
Gemfile
gem 'jbuilder', '~> 2.5' gem 'active_model_serializers', '~> 0.10.0'
Então executemos:
bundle install
Usando a Gem jBuilder
jBuilder é uma gem popular mantida pela equipe do Rails que provê uma DSL simples (domain-specific language), permitindo-nos definir estruturas JSON nas visões.
Suponhamos que queremos mostrar todas as publicações quando o usuário acessar a ação index
:
controllers/api/v1/posts_controller.rb
def index @posts = Post.order('created_at DESC') end
Tudo que precisamos é criar uma visão com o nome da ação correspondente, com a extensão .json.jbuilder. Notemos que a visão deve estar no caminho api/v1 também:
views/api/v1/posts/index.json.jbuilder
json.array! @posts do |post| json.id post.id json.title post.title json.body post.body end
json.array!
percorre o vetor @posts
. json.id
, json.title
e json.body
geram as chaves com os nomes correspondentes, usando argumentos como valor. Se navegarmos até http://localhost:3000/api/v1/posts.json, veremos um retorno equivalente a esse:
[ {"id": 1, "title": "Title 1", "body": "Body 1"}, {"id": 2, "title": "Title 2", "body": "Body 2"} ]
E se quisermos mostrar o autor de cada publicação? É simples:
json.array! @posts do |post| json.id post.id json.title post.title json.body post.body json.user do json.id post.user.id json.name post.user.name end end
O retorno mudará para:
[ {"id": 1, "title": "Title 1", "body": "Body 1", "user": {"id": 1, "name": "Username"}} ]
O conteúdo do arquivo .jbuilder é puro Ruby, então podemos usar todas as operações básicas tradicionais.
Notemos que jBuilder suporta parciais como qualquer outra visão Rails, então podemos dizer:
json.partial! partial: 'posts/post', collection: @posts, as: :post
e criar o arquivo views/api/v1/posts/_post.json.jbuilder com o conteúdo a seguir:
json.id post.id json.title post.title json.body post.body json.user do json.id post.user.id json.name post.user.name end
Assim, como vemos, jBuilder é fácil e conveniente. Contudo, como alternativa, podemos usar serializadores, então discutamo-nos na próxima seção.
Usando Serializadores
A gem rails_model_serializers foi criada pela equipe que antes administrava a rails-api. Como dito na documentação, rails_model_serializers trás convenção acima de configuração para geração de JSON. Basicamente, definimos que campos devem ser usados na serialização (isso, geração do JSON).
Eis nosso primeiro serializador:
serializers/post_serializer.rb
class PostSerializer < ActiveModel::Serializer attributes :id, :title, :body end
Aqui dizemos que todos os campos estarão presentes no JSON final. Agora, to_json
ou as_json
quando invocados numa publicação, usarão essa configuração o retorno correto.
Para vermos em ação, modifiquemos a ação index
, assim:
controllers/api/v1/posts_controller.rb
def index @posts = Post.order('created_at DESC') render json: @posts end
as_json
chamará automaticamente no objeto @posts
.
E os usuários? Serializadores permitem-nos indicar relações, como modelos. E eles também podem ser aninhados:
serializers/post_serializer.rb
class PostSerializer < ActiveModel::Serializer attributes :id, :title, :body belongs_to :user class UserSerializer < ActiveModel::Serializer attributes :id, :name end end
Agora, ao serializar uma publicação, ela conterá a chave user
com o id e nome. Se depois criarmos uma serializador separado para o usuário sem o atributo :id
:
serializers/post_serializer.rb
class UserSerializer < ActiveModel::Serializer attributes :name end
então @user.as_json
não retornará o id do usuário. Todavia, @post.as_json
retornará tanto o nome quanto o id do usuário, então lembre-se.
Protegendo a API
Várias vezes, não queremos alguém realizando qualquer ação na API. Então criemos uma verificação de segurança e forcemos os usuários a enviar seus tokens ao criar e remover publicação.
O token terá um tempo indeterminado e será criado no registro do usuário. Primeiro, adicionemos uma coluna token
à tabela users
:
rails g migration add_token_to_users token:string:index
Esse índice deve garantir unicidade já que não podemos ter dois usuários com o mesmo token:
db/migrate/xyz_add_token_to_users.rb
add_index :users, :token, unique: true
Apliquemos a migração:
rails db:migrate
Agora, adicionemos a callback before_save
:
models/user.rb
before_create -> {self.token = generate_token}
O método privado generate_token
criará um toke em um ciclo infinito e verificará se é único ou não. tão logo o token único é achado, retornamo-no:
models/user.rb
private def generate_token loop do token = SecureRandom.hex return token unless User.exists?({token: token}) end end
Podemos usar outro algoritmo para a geração do toke, por exemplo, baseado num hash MD5 do nome do usuário e algum sal.
Registro de Usuários
Claro, também precisamos permitir usuários se registrarem ou não poderão obter seus tokens. Não queremos introduzir quaisquer visões HTML à aplicação, então, adicionemos um novo método à API:
controllers/api/v1/users_controller.rb
def create @user = User.new(user_params) if @user.save render status: :created else render json: @user.errors, status: :unprocessable_entity end end private def user_params params.require(:user).permit(:name) end
É uma boa ideia retornar códigos de estado HTTP significativos para desenvolvedores entenderem o que se passa. Agora, podemos prover um novo serializador para os usuários ou continuar com o .json.jbuilder. Preferimos o último (por isso não passamos a opção :json
para render
), mas somos livres para escolher qualquer um. Notemos, contudo, que o token não deve ser sempre serializado, por exemplo, quando retornarmos uma lista de todos os usuários—deve ficar fora e seguro!
views/api/v1/users/create.json.jbuilder
json.id @user.id json.name @user.name json.token @user.token
O próximo passo é testar se tudo funciona. Podemos usar cURL
ou criar um código em Ruby. Já que usamos Ruby aqui, vamos com a segunda opção.
Testando o Registro de Usuários
Para realizar uma requisição HTTP, usaremos a gem Faraday, que provê uma interface comum para vários adaptadores (o padrão é Net::HTTP
). Criemos um arquivo Ruby separado, incluindo Faraday, e configuremos o cliente:
api_client.rb
require 'faraday' client = Faraday.new(url: 'http://localhost:3000') do |config| config.adapter Faraday.default_adapter end response = client.post do |req| req.url '/api/v1/users' req.headers['Content-Type'] = 'application/json' req.body = '{ "user": {"name": "test user"} }' end
Todas as opções são auto-explicativas: escolher o adaptador padrão, configuramos a URL da requisição para http://locahost:3000/api/v1/users, mudamos o tipo do conteúdo para application/json
e provemos o corpo da requisição.
A resposta do servidor conterá JSON, então analisemo-no com a gem Oj:
api_client.rb
require 'oj' # client here... puts Oj.load(response.body) puts response.status
Além da resposta analisada, também mostramos o código de estado para possível depuração.
Agora, podemos apenas executar o script:
ruby api_client.rb
e salvar o token recebido em algum lugar—usaremo-no na próxima seção.
Autenticação Com o Token
Para forçar autenticação com toke, o método authenticate_or_request_with_http_token
pode ser usado. Faz parte do módulo ActionController::HttpAuthentication::Token::ControllerMethods então não esqueçamos de incluí-lo:
controllers/api/v1/posts_controller.rb
class PostsController < ApplicationController include ActionController::HttpAuthentication::Token::ControllerMethods # ... end
Adicionemos before_action
e o método correspondente:
controllers/api/v1/posts_controller.rb
before_action :authenticate, only: [:create, :destroy] # ... private # ... def authenticate authenticate_or_request_with_http_token do |token, options| @user = User.find_by(token: token) end end
Agora, se o token não for configurado ou se o usuário com o token não for encontrado, um erro 401 será retornado, parando a execução da ação.
Notemos que a comunicação entre o cliente e o servidor tem de ser via HTTPS, ou os tokens podem ser falsificados. Claro, a solução provida não é ideal, e em vários casos é preferível aplicar o protocolo OAuth 2 para autenticação. Há, pelo menos, duas gems para simplificar bastante o processo de suportar esse recurso: Doorkeeper e oPRO.
Criando uma Publicação
Para ver a autenticação em ação, adicionemos a ação create
a PostsController
:
controllers/api/v1/posts_controller.rb
def create @post = @user.posts.new(post_params) if @post.save render json: @post, status: :created else render json: @post.errors, status: :unprocessable_entity end end
Lançamos mão do serializador aqui para mostrar o JSON apropriado. O @user
já foi configurado dentro de before_action
.
Agora, testemos tudo usando esse código abaixo:
api_client.rb
client = Faraday.new(url: 'http://localhost:3000') do |config| config.adapter Faraday.default_adapter config.token_auth('127a74dbec6f156401b236d6cb32db0d') end response = client.post do |req| req.url '/api/v1/posts' req.headers['Content-Type'] = 'application/json' req.body = '{ "post": {"title": "Title", "body": "Text"} }' end
Substituamos o argumento passado para token_auth
com o token recebido no registro e executemos o script.
ruby api_client.rb
Apagando uma Publicação
Apagar uma publicação é da mesma forma. Adicionemos a ação destroy
:
controllers/api/v1/posts_controller.rb
def destroy @post = @user.posts.find_by(params[:id]) if @post @post.destroy else render json: {post: "not found"}, status: :not_found end end
Só permitimos usuários apagarem publicação que criaram. Se foi removida com sucesso, retornarmos o código de estado 204 (sem conteúdo). Também podemos, responder com o id da publicação apagada, já que continuará disponível na memória.
Eis o código que testará esse novo recurso:
api_client.rb
response = client.delete do |req| req.url '/api/v1/posts/6' req.headers['Content-Type'] = 'application/json' end
Mudemos o id da publicação com aquele que servir para nós.
Configurando CORS
Para permitir que outros serviços web acessem nossa API (via lado cliente), é preciso configurar CORS (Cross-Origin Resource Sharing) apropriadamente. Basicamente, CORS permite que aplicações web enviem requisições AJAX a serviços de terceiros. Felizmente, há uma gem chamada rack-cors que nos permite configurar tudo facilmente. Adicionemo-na ao Gemfile:
Gemfile
gem 'rack-cors'
Instalemo-na:
bundle install
E forneçamos a configuração dentro do arquivo config/initializers/cors.rb. Na verdade, já foi criado para nós e contém um exemplo de uso. Também encontramos uma documentação bem detalhada na página da gem.
A configuração abaixo, por exemplo, permitirá acesso à API por qualquer um usando qualquer método:
config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins '*' resource '/api/*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end
Prevenindo Abusos
A última coisa que mencionaremos nesse guia é como proteger a API de abusos e ataques de negação de serviço. Há uma gem muito legal, rack-attack (criada pelo Kickstarte) que nos permite colocar clientes na lista negra ou branca, prevenindo a inundação de requisições a um servidor e mais.
Coloquemos a gem no Gemfile:
Gemfile
gem 'rack-attack'
Instalemo-na:
bundle install
E fornecemos a configuração dentro do arquivo inicializador rack_attack.rb. A documentação da gem lista todas as opções disponíveis e sugere alguns casos de uso. Eis uma configuração exemplo que restringe qualquer um exceto nós de acessar o serviço e limita o número de requisições a 5 por segundo:
config/initializers/rack_attack.rb
class Rack::Attack safelist('allow from localhost') do |req| # Requests are allowed if the return value is truthy '127.0.0.1' == req.ip || '::1' == req.ip end throttle('req/ip', :limit => 5, :period => 1.second) do |req| req.ip end end
Outra coisa que precisamos fazer é incluir RackAttack como mediador:
config/application.rb
config.middleware.use Rack::Attack
Conclusão
Chegamos ao fim do artigo. Esperamos que se sinta confiante em criar APIs com Rails! Vale lembrar que essa não é a única opção—outra solução popular que existe há um tempo é o framework Grape, então vale verificá-lo também.
Não hesitem em deixar suas dúvidas se algo não estiver claro. Agradecemos por lerem tudo e ótima programação!
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post