# Como consumir API de CPF em Turbo (Hotwire) com frames e streams

> Saiba como consumir a API de CPF da CPFHub usando Turbo Frames e Turbo Streams do Hotwire para validação dinâmica em Rails.

**Publicado:** 28/07/2026
**Autor:** Redação CPFHub.io
**URL:** https://cpfhub.io/blog/como-consumir-api-de-cpf-em-turbo-hotwire-com-frames-e-streams

---


Para consumir a API de CPF da CPFHub em aplicações Rails com Hotwire, crie um controller que faz uma requisição GET para `https://api.cpfhub.io/cpf/{CPF}` com o header `x-api-key` e responda com um template Turbo Frame ou Turbo Stream. Turbo Frames atualizam um fragmento isolado da página, enquanto Turbo Streams permitem atualizar múltiplas regiões simultaneamente — ambos sem recarregar a página inteira. A latência média da API é de aproximadamente 900ms, o que torna o uso de um indicador de carregamento importante para uma boa experiência do usuário. Consulte a [documentação oficial do Turbo](https://turbo.hotwired.dev) para entender as diferenças entre frames e streams antes de escolher a abordagem.

---

## Turbo Frames vs Turbo Streams

### Turbo Frames

Turbo Frames encapsulam uma seção da página que pode ser atualizada independentemente. Quando um formulário dentro de um frame é submetido, apenas o conteúdo do frame é substituído pela resposta do servidor.

### Turbo Streams

Turbo Streams permitem atualizações granulares do DOM usando ações como `append`, `replace`, `update` e `remove`. Podem ser enviados via resposta HTTP ou via WebSocket com Action Cable.

### Qual usar para validação de CPF

Para a maioria dos casos, Turbo Frames são suficientes. Use Turbo Streams quando precisar atualizar múltiplas regiões da página simultaneamente -- por exemplo, preencher nome, data de nascimento e status de validação em áreas diferentes do formulário.

---

## Configurando o controller Rails

Primeiro, crie o controller que receberá as requisições de validação e consumirá a API da CPFHub:

```ruby
# app/controllers/cpf_lookups_controller.rb
class CpfLookupsController < ApplicationController
 require "net/http"
 require "json"

 def create
 cpf = params[:cpf].to_s.gsub(/\D/, "")

 if cpf.length != 11
 @error = "CPF deve conter 11 dígitos."
 respond_to do |format|
 format.turbo_stream
 format.html { render :new }
 end
 return
 end

 @result = fetch_cpf_data(cpf)

 respond_to do |format|
 format.turbo_stream
 format.html { render :new }
 end
 end

 private

 def fetch_cpf_data(cpf)
 uri = URI("https://api.cpfhub.io/cpf/#{cpf}")
 http = Net::HTTP.new(uri.host, uri.port)
 http.use_ssl = true
 http.open_timeout = 10
 http.read_timeout = 10

 request = Net::HTTP::Get.new(uri)
 request["x-api-key"] = Rails.application.credentials.cpfhub_api_key
 request["Accept"] = "application/json"

 response = http.request(request)
 JSON.parse(response.body, symbolize_names: true)
 rescue Net::OpenTimeout, Net::ReadTimeout
 { success: false, error: "Tempo de resposta excedido" }
 rescue StandardError => e
 { success: false, error: "Erro na consulta: #{e.message}" }
 end
end
```

---

## Implementando com Turbo Frames

### A view do formulário

Crie o formulário envolvido por um Turbo Frame:

```erb
<%# app/views/cpf_lookups/new.html.erb %>
<div class="cpf-lookup-container">
 <h2>Consulta de CPF</h2>

 <%= form_with url: cpf_lookups_path, method: :post, data: { turbo_frame: "cpf_result" } do |f| %>
 <div class="form-group">
 <%= f.label :cpf, "Digite o CPF" %>
 <%= f.text_field :cpf,
 placeholder: "000.000.000-00",
 inputmode: "numeric",
 autofocus: true %>
 </div>

 <%= f.submit "Consultar", class: "btn btn-primary" %>
 <% end %>

 <%= turbo_frame_tag "cpf_result" do %>
 <div class="cpf-result-placeholder">
 <p>Os dados do CPF aparecerão aqui após a consulta.</p>
 </div>
 <% end %>
</div>
```

### A resposta em Turbo Frame

```erb
<%# app/views/cpf_lookups/create.html.erb %>
<%= turbo_frame_tag "cpf_result" do %>
 <% if @error %>
 <div class="alert alert-danger">
 <p><%= @error %></p>
 </div>
 <% elsif @result && @result[:success] %>
 <div class="cpf-result cpf-result--success">
 <h3>Dados encontrados</h3>
 <dl>
 <dt>CPF</dt>
 <dd><%= @result[:data][:cpf] %></dd>
 <dt>Nome</dt>
 <dd><%= @result[:data][:name] %></dd>
 <dt>Data de Nascimento</dt>
 <dd><%= @result[:data][:birthDate] %></dd>
 <dt>Gênero</dt>
 <dd><%= @result[:data][:gender] %></dd>
 </dl>
 </div>
 <% else %>
 <div class="alert alert-warning">
 <p>CPF não encontrado ou erro na consulta.</p>
 </div>
 <% end %>
<% end %>
```

---

## Implementando com Turbo Streams

Para cenários onde você precisa atualizar múltiplas partes da página, use Turbo Streams:

### A resposta em Turbo Stream

```erb
<%# app/views/cpf_lookups/create.turbo_stream.erb %>
<% if @error %>
 <%= turbo_stream.update "cpf_feedback" do %>
 <div class="alert alert-danger"><%= @error %></div>
 <% end %>

 <%= turbo_stream.update "cpf_name_field" do %>
 <%= text_field_tag :name, "", readonly: true, class: "form-control" %>
 <% end %>

<% elsif @result && @result[:success] %>
 <%= turbo_stream.update "cpf_feedback" do %>
 <div class="alert alert-success">CPF válido!</div>
 <% end %>

 <%= turbo_stream.update "cpf_name_field" do %>
 <%= text_field_tag :name, @result[:data][:name],
 readonly: true, class: "form-control" %>
 <% end %>

 <%= turbo_stream.update "cpf_birthdate_field" do %>
 <%= text_field_tag :birth_date, @result[:data][:birthDate],
 readonly: true, class: "form-control" %>
 <% end %>

 <%= turbo_stream.update "cpf_gender_field" do %>
 <%= text_field_tag :gender, @result[:data][:gender],
 readonly: true, class: "form-control" %>
 <% end %>

<% else %>
 <%= turbo_stream.update "cpf_feedback" do %>
 <div class="alert alert-warning">CPF não encontrado.</div>
 <% end %>
<% end %>
```

### O formulário com targets para Turbo Streams

```erb
<%# app/views/registrations/new.html.erb %>
<%= form_with url: registrations_path, method: :post do |f| %>
 <div class="form-group">
 <%= f.label :cpf %>
 <div class="input-group">
 <%= f.text_field :cpf, id: "cpf_input", inputmode: "numeric" %>
 <%= button_tag "Validar",
 type: "button",
 data: { action: "click->cpf-turbo#submit" },
 class: "btn btn-secondary" %>
 </div>
 <div id="cpf_feedback"></div>
 </div>

 <div class="form-group">
 <%= f.label :name, "Nome" %>
 <div id="cpf_name_field">
 <%= f.text_field :name, readonly: true %>
 </div>
 </div>

 <div class="form-group">
 <%= f.label :birth_date, "Data de Nascimento" %>
 <div id="cpf_birthdate_field">
 <%= f.text_field :birth_date, readonly: true %>
 </div>
 </div>

 <div class="form-group">
 <%= f.label :gender, "Gênero" %>
 <div id="cpf_gender_field">
 <%= f.text_field :gender, readonly: true %>
 </div>
 </div>

 <%= f.submit "Cadastrar" %>
<% end %>
```

---

## Turbo Streams via Action Cable em tempo real

Para cenários avançados -- como validações em lote ou dashboards --, use Turbo Streams com WebSocket:

```ruby
# app/models/cpf_lookup.rb
class CpfLookup < ApplicationRecord
 after_create_commit -> {
 broadcast_append_to "cpf_lookups",
 partial: "cpf_lookups/lookup",
 locals: { lookup: self },
 target: "cpf_lookups_list"
 }
end
```

```erb
<%# app/views/cpf_lookups/index.html.erb %>
<%= turbo_stream_from "cpf_lookups" %>

<div id="cpf_lookups_list">
 <%= render @cpf_lookups %>
</div>
```

```erb
<%# app/views/cpf_lookups/_lookup.html.erb %>
<div class="lookup-item" id="<%= dom_id(lookup) %>">
 <span class="lookup-cpf"><%= lookup.cpf %></span>
 <span class="lookup-name"><%= lookup.name %></span>
 <span class="lookup-status <%= lookup.valid? ? 'success' : 'error' %>">
 <%= lookup.valid? ? "Válido" : "Inválido" %>
 </span>
</div>
```

---

## Adicionando loading state com Stimulus auxiliar

Para mostrar um indicador de carregamento durante a requisição Turbo, crie um controller Stimulus complementar:

```javascript
// app/javascript/controllers/cpf_turbo_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
 static targets = ["form", "spinner", "button"]

 submit() {
 const cpfInput = document.getElementById("cpf_input")
 const cpf = cpfInput.value.replace(/\D/g, "")

 if (cpf.length !== 11) {
 document.getElementById("cpf_feedback").innerHTML =
 '<div class="alert alert-warning">CPF deve ter 11 dígitos.</div>'
 return
 }

 this.showLoading()

 const form = document.createElement("form")
 form.method = "POST"
 form.action = "/cpf_lookups"
 form.dataset.turbo = "true"

 const csrfToken = document.querySelector("meta[name='csrf-token']").content
 const csrfInput = document.createElement("input")
 csrfInput.type = "hidden"
 csrfInput.name = "authenticity_token"
 csrfInput.value = csrfToken

 const cpfField = document.createElement("input")
 cpfField.type = "hidden"
 cpfField.name = "cpf"
 cpfField.value = cpf

 form.appendChild(csrfInput)
 form.appendChild(cpfField)
 document.body.appendChild(form)

 form.requestSubmit()

 setTimeout(() => {
 document.body.removeChild(form)
 this.hideLoading()
 }, 5000)
 }

 showLoading() {
 if (this.hasSpinnerTarget) {
 this.spinnerTarget.classList.remove("hidden")
 }
 if (this.hasButtonTarget) {
 this.buttonTarget.disabled = true
 }
 }

 hideLoading() {
 if (this.hasSpinnerTarget) {
 this.spinnerTarget.classList.add("hidden")
 }
 if (this.hasButtonTarget) {
 this.buttonTarget.disabled = false
 }
 }
}
```

---

## Configurando as rotas

Não esqueça de configurar as rotas:

```ruby
# config/routes.rb
Rails.application.routes.draw do
 resources :cpf_lookups, only: [:new, :create, :index]
 resources :registrations, only: [:new, :create]
end
```

---

## Testando a integração

Utilize testes de sistema para verificar o fluxo completo:

```ruby
# test/system/cpf_lookups_test.rb
require "application_system_test_case"

class CpfLookupsTest < ApplicationSystemTestCase
 test "validating a CPF via Turbo Frame" do
 visit new_cpf_lookup_path

 fill_in "CPF", with: "123.456.789-09"
 click_button "Consultar"

 assert_selector "#cpf_result .cpf-result--success", wait: 15
 assert_text "Dados encontrados"
 end
end
```

---

## Perguntas frequentes

### Como funciona a autenticação na API CPFHub.io dentro de uma aplicação Rails?

A autenticação é feita pelo header `x-api-key` em cada requisição GET para `https://api.cpfhub.io/cpf/{CPF}`. Em Rails, o valor da chave deve ser armazenado em `credentials.yml.enc` e acessado via `Rails.application.credentials.cpfhub_api_key`, nunca em texto puro no código-fonte. Isso impede que a chave seja exposta em repositórios Git.

### Turbo Frames ou Turbo Streams: qual escolher para validação de CPF?

Use Turbo Frames quando precisar atualizar apenas uma região isolada da página — como um bloco de resultado abaixo do campo de CPF. Use Turbo Streams quando a resposta da API precisar preencher múltiplos campos simultâneos, como nome, data de nascimento e status em partes diferentes do formulário. Para a maioria dos cadastros simples, Turbo Frames são suficientes e mais simples de implementar.

### A API CPFHub.io bloqueia requisições quando o limite do plano é atingido?

Não. Quando o limite mensal é ultrapassado, a API continua respondendo normalmente e cobra R$0,15 por consulta adicional — sem retornar erro 429 nem bloquear o serviço. O plano gratuito inclui 50 consultas por mês; o plano Pro oferece 1.000 consultas mensais por R$149.

### Qual o tempo de resposta esperado da API e como isso afeta a experiência no Turbo?

A latência média da API CPFHub.io é de aproximadamente 900ms. Por isso, exibir um indicador de carregamento durante a requisição Turbo é importante para evitar que o usuário pense que a página travou. O controller Stimulus auxiliar descrito neste artigo implementa exatamente esse comportamento com `showLoading()` e `hideLoading()`.

### Leia também

- [Como validar CPF no frontend com React e API REST](https://cpfhub.io/blog/como-validar-cpf-no-frontend-com-react-e-api-rest)
- [Boas práticas para consumir APIs de CPF de forma segura](https://cpfhub.io/blog/boas-praticas-consumir-apis-cpf-segura)
- [Autenticação em APIs REST: como garantir segurança na consulta de CPF](https://cpfhub.io/blog/autenticacao-apis-rest-seguranca-consulta-cpf)
- [Como consumir API de CPF em Ruby on Rails com Faraday](https://cpfhub.io/blog/como-consumir-api-de-cpf-em-ruby-on-rails-com-faraday)

---

## Conclusão

Turbo Frames e Turbo Streams oferecem uma maneira poderosa e elegante de integrar validações de CPF em aplicações Rails sem escrever JavaScript complexo. Com Turbo Frames, atualize seções isoladas do formulário após a consulta. Com Turbo Streams, atualize múltiplas áreas da página simultaneamente. E com Action Cable, receba atualizações em tempo real para dashboards e validações em lote.

A API do [**CPFHub.io**](https://www.cpfhub.io/)

Cadastre-se em [cpfhub.io](https://www.cpfhub.io/)

