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.

Redação CPFHub.io
Redação CPFHub.io
··8 min de leitura
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 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:

# 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:

<%# 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

<%# 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

<%# 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

<%# 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:

# 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
<%# app/views/cpf_lookups/index.html.erb %>
<%= turbo_stream_from "cpf_lookups" %>

<div id="cpf_lookups_list">
    <%= render @cpf_lookups %>
</div>
<%# 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:

// 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:

# 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:

# 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().


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

Cadastre-se em cpfhub.io

CPFHub.io

Pronto para integrar a API?

50 consultas gratuitas para testar agora. Sem cartão de crédito. Acesso imediato à documentação.

Redação CPFHub.io

Sobre a redação

Redação CPFHub.io

Time editorial especializado em APIs de CPF, identidade digital e compliance no mercado brasileiro. Produzimos guias técnicos, análises regulatórias e tutoriais sobre LGPD e KYC para desenvolvedores e líderes de produto.

WhatsAppFale conosco via WhatsApp