# Como integrar validação de CPF em Stimulus (Hotwire/Rails) com controllers

> Aprenda a integrar validação de CPF em aplicações Rails usando Stimulus controllers do Hotwire com chamadas à API da CPFHub.

**Publicado:** 27/07/2026
**Autor:** Redação CPFHub.io
**URL:** https://cpfhub.io/blog/como-integrar-validacao-de-cpf-em-stimulus-hotwire-rails-com-controllers

---


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](https://stimulus.hotwired.dev) 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.

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

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

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

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

```erb
<%= 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:

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

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

```css
/* 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:

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

```ruby
# 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.

---

### 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)
- [Como consumir API de CPF em Turbo (Hotwire) com frames e streams](https://cpfhub.io/blog/como-consumir-api-de-cpf-em-turbo-hotwire-com-frames-e-streams)
- [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

A integração do Stimulus com a API do [**CPFHub.io**](https://www.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](https://www.cpfhub.io/)

