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.

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

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