Com o Stimulus (a camada JavaScript do Hotwire), é possível integrar validação de CPF em tempo real em formulários Rails sem abrir mão da simplicidade do ecossistema. Você cria um controller reutilizável que detecta a entrada do usuário, faz uma chamada ao backend Rails e exibe o resultado diretamente no HTML — tudo em aproximadamente 900ms de latência da API da CPFHub.io. A chave de API permanece no servidor, e o controller pode ser reaproveitado em qualquer view da aplicação. Consulte a documentação oficial do Stimulus para entender o ciclo de vida dos controllers antes de começar.
Por que usar Stimulus para validação de CPF
Organização com controllers
O Stimulus organiza o JavaScript em controllers reutilizáveis, conectados ao DOM por meio de atributos data-*. Isso significa que o comportamento de validação de CPF pode ser encapsulado em um único controller e reutilizado em qualquer formulário da aplicação.
Benefícios da abordagem
- Separação de responsabilidades: o HTML define a estrutura, o controller cuida da lógica.
- Reutilização: um controller pode ser usado em múltiplas views sem duplicação de código.
- Testabilidade: controllers Stimulus podem ser testados de forma isolada.
- Compatibilidade com Turbo: funciona perfeitamente com Turbo Drive e Turbo Frames.
Configurando o ambiente Rails com Hotwire
Antes de criar o controller, certifique-se de que o Hotwire está instalado no seu projeto Rails. Em projetos Rails 7+, o Hotwire já vem configurado por padrão.
# Para projetos existentes sem Hotwire
bundle add hotwire-rails
rails hotwire:install
A estrutura de diretórios para controllers Stimulus fica em app/javascript/controllers/. Cada controller é um arquivo JavaScript que exporta uma classe estendendo Controller do Stimulus.
Criando o Stimulus controller de validação de CPF
Crie o arquivo app/javascript/controllers/cpf_validation_controller.js com o seguinte conteúdo:
// app/javascript/controllers/cpf_validation_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "feedback", "name", "birthDate", "gender", "submitButton"]
static values = {
apiKey: String,
debounce: { type: Number, default: 500 }
}
connect() {
this.timeout = null
}
disconnect() {
if (this.timeout) clearTimeout(this.timeout)
}
validate() {
if (this.timeout) clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.performValidation()
}, this.debounceValue)
}
async performValidation() {
const cpf = this.inputTarget.value.replace(/\D/g, "")
if (cpf.length !== 11) {
this.showFeedback("Digite um CPF válido com 11 dígitos.", "warning")
return
}
if (!this.isValidCpfFormat(cpf)) {
this.showFeedback("CPF com formato inválido.", "error")
return
}
this.showFeedback("Validando CPF...", "loading")
this.disableSubmit()
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000)
try {
const response = await fetch(`https://api.cpfhub.io/cpf/${cpf}`, {
method: "GET",
headers: {
"x-api-key": this.apiKeyValue,
"Accept": "application/json"
},
signal: controller.signal
})
clearTimeout(timeoutId)
if (!response.ok) {
throw new Error(`Erro na requisição: ${response.status}`)
}
const result = await response.json()
if (result.success) {
this.showFeedback("CPF válido!", "success")
this.fillData(result.data)
this.enableSubmit()
} else {
this.showFeedback("CPF não encontrado na base de dados.", "error")
}
} catch (error) {
clearTimeout(timeoutId)
if (error.name === "AbortError") {
this.showFeedback("Tempo de resposta excedido. Tente novamente.", "error")
} else {
this.showFeedback("Erro ao validar CPF. Tente novamente.", "error")
}
}
}
isValidCpfFormat(cpf) {
if (/^(\d)\1{10}$/.test(cpf)) return false
let sum = 0
for (let i = 0; i < 9; i++) sum += parseInt(cpf.charAt(i)) * (10 - i)
let remainder = (sum * 10) % 11
if (remainder === 10) remainder = 0
if (remainder !== parseInt(cpf.charAt(9))) return false
sum = 0
for (let i = 0; i < 10; i++) sum += parseInt(cpf.charAt(i)) * (11 - i)
remainder = (sum * 10) % 11
if (remainder === 10) remainder = 0
return remainder === parseInt(cpf.charAt(10))
}
fillData(data) {
if (this.hasNameTarget) this.nameTarget.value = data.name
if (this.hasBirthDateTarget) this.birthDateTarget.value = data.birthDate
if (this.hasGenderTarget) this.genderTarget.value = data.gender
}
showFeedback(message, type) {
if (!this.hasFeedbackTarget) return
this.feedbackTarget.textContent = message
this.feedbackTarget.className = `cpf-feedback cpf-feedback--${type}`
}
disableSubmit() {
if (this.hasSubmitButtonTarget) {
this.submitButtonTarget.disabled = true
}
}
enableSubmit() {
if (this.hasSubmitButtonTarget) {
this.submitButtonTarget.disabled = false
}
}
}
Conectando o controller ao HTML
Com o controller criado, conecte-o ao formulário usando os atributos data-* do Stimulus:
<%# app/views/users/_form.html.erb %>
<%= form_with model: @user,
data: {
controller: "cpf-validation",
cpf_validation_api_key_value: Rails.application.credentials.cpfhub_api_key
} do |f| %>
<div class="form-group">
<%= f.label :cpf, "CPF" %>
<%= f.text_field :cpf,
data: {
cpf_validation_target: "input",
action: "input->cpf-validation#validate"
},
placeholder: "000.000.000-00",
inputmode: "numeric" %>
<span data-cpf-validation-target="feedback"></span>
</div>
<div class="form-group">
<%= f.label :name, "Nome Completo" %>
<%= f.text_field :name,
data: { cpf_validation_target: "name" },
readonly: true %>
</div>
<div class="form-group">
<%= f.label :birth_date, "Data de Nascimento" %>
<%= f.text_field :birth_date,
data: { cpf_validation_target: "birthDate" },
readonly: true %>
</div>
<div class="form-group">
<%= f.label :gender, "Gênero" %>
<%= f.text_field :gender,
data: { cpf_validation_target: "gender" },
readonly: true %>
</div>
<%= f.submit "Cadastrar",
data: { cpf_validation_target: "submitButton" },
disabled: true %>
<% end %>
Adicionando máscara de CPF com Stimulus
Para melhorar a experiência do usuário, adicione formatação automática ao campo de CPF. Crie um controller auxiliar ou adicione a funcionalidade ao controller existente:
// Adicione ao cpf_validation_controller.js
format() {
let value = this.inputTarget.value.replace(/\D/g, "")
if (value.length > 11) value = value.slice(0, 11)
if (value.length > 9) {
value = value.replace(/(\d{3})(\d{3})(\d{3})(\d{1,2})/, "$1.$2.$3-$4")
} else if (value.length > 6) {
value = value.replace(/(\d{3})(\d{3})(\d{1,3})/, "$1.$2.$3")
} else if (value.length > 3) {
value = value.replace(/(\d{3})(\d{1,3})/, "$1.$2")
}
this.inputTarget.value = value
}
No HTML, adicione a action de formatação:
<%= f.text_field :cpf,
data: {
cpf_validation_target: "input",
action: "input->cpf-validation#format input->cpf-validation#validate"
} %>
Validação no backend com Rails
A validação no frontend é importante para a experiência do usuário, mas nunca dispense a validação no servidor. Crie um serviço Ruby para consultar a API:
# app/services/cpf_validation_service.rb
class CpfValidationService
include HTTParty
base_uri "https://api.cpfhub.io"
def initialize
@api_key = Rails.application.credentials.cpfhub_api_key
end
def validate(cpf)
cleaned = cpf.gsub(/\D/, "")
response = self.class.get(
"/cpf/#{cleaned}",
headers: {
"x-api-key" => @api_key,
"Accept" => "application/json"
},
timeout: 10
)
if response.success?
JSON.parse(response.body, symbolize_names: true)
else
{ success: false, error: "Falha na validação" }
end
rescue Net::OpenTimeout, Net::ReadTimeout
{ success: false, error: "Tempo de resposta excedido" }
end
end
No model, utilize o serviço:
# app/models/user.rb
class User < ApplicationRecord
validate :cpf_must_be_valid, on: :create
private
def cpf_must_be_valid
service = CpfValidationService.new
result = service.validate(cpf)
unless result[:success]
errors.add(:cpf, "não é válido ou não foi encontrado")
end
end
end
Tratamento de erros e estados de loading
Um bom controller Stimulus deve tratar todos os estados possíveis. Adicione estilos CSS para os diferentes estados de feedback:
/* app/assets/stylesheets/cpf_validation.css */
.cpf-feedback {
display: block;
margin-top: 0.25rem;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.cpf-feedback--loading {
color: #6b7280;
}
.cpf-feedback--success {
color: #059669;
}
.cpf-feedback--error {
color: #dc2626;
}
.cpf-feedback--warning {
color: #d97706;
}
.cpf-feedback--loading::before {
content: "";
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid #6b7280;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin-right: 0.5rem;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
Boas práticas e segurança
Protegendo a chave de API
Nunca exponha a chave de API diretamente no HTML em produção. Utilize uma rota intermediária no Rails:
# config/routes.rb
post "/api/validate_cpf", to: "cpf_validations#create"
# app/controllers/cpf_validations_controller.rb
class CpfValidationsController < ApplicationController
def create
service = CpfValidationService.new
result = service.validate(params[:cpf])
render json: result
end
end
Rate limiting
Adicione rate limiting ao endpoint para evitar abusos:
# Gemfile
gem "rack-attack"
# config/initializers/rack_attack.rb
Rack::Attack.throttle("cpf_validation", limit: 10, period: 60) do |req|
req.ip if req.path == "/api/validate_cpf" && req.post?
end
Perguntas frequentes
Como o Stimulus controller de CPF se integra com Turbo Drive no Rails?
O Stimulus foi projetado para funcionar com Turbo Drive: quando o usuário navega entre páginas via Turbo, o controller é desconectado e reconectado automaticamente via os callbacks connect() e disconnect(). Isso garante que timeouts e event listeners sejam limpos corretamente, sem memory leaks entre navegações.
A API da CPFHub.io retorna erro 429 quando o limite de consultas é atingido?
Não. Ao atingir o limite do plano, a API continua respondendo normalmente e cobra R$0,15 por consulta adicional — sem bloquear. Por isso, o rate limiting com rack-attack no controller Rails protege seu orçamento, não a disponibilidade da API em si.
Qual a diferença entre validar o CPF no Stimulus controller e validar no model Rails?
O controller Stimulus valida no frontend para dar feedback imediato ao usuário durante o preenchimento do formulário, reduzindo erros antes do submit. A validação no model Rails garante integridade dos dados independentemente de como a requisição chega — seja via formulário HTML, API ou importação em lote. As duas camadas são complementares.
Como lidar com a latência de ~900ms da API sem prejudicar a experiência do usuário?
O debounce de 500ms no validate() evita chamadas a cada tecla digitada. O estado de loading com a classe cpf-feedback--loading e o spinner CSS mostram ao usuário que a consulta está em andamento. Com o AbortController e timeout de 10 segundos, a interface não trava mesmo se a rede estiver lenta.
Conclusão
A integração do Stimulus com a API do CPFHub.io
A API da CPFHub retorna respostas em aproximadamente 900ms, com uptime de 99,9% e total conformidade com a LGPD -- ideal para aplicações Rails em produção.
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.
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.



