Como consumir API de CPF em Alpine.js com fetch e x-data

Aprenda a consumir a API de CPF da CPFHub.io em Alpine.js usando fetch e diretivas x-data para interfaces reativas e leves.

Redação CPFHub.io
Redação CPFHub.io
··9 min de leitura
Como consumir API de CPF em Alpine.js com fetch e x-data

O Alpine.js permite consumir a API de CPF da CPFHub.io com reatividade declarativa diretamente no HTML, sem build steps e sem frameworks pesados. Com x-data gerenciando o estado e fetch fazendo a chamada ao backend proxy, a consulta acontece em tempo real enquanto o usuário digita — com latência de aproximadamente 900ms da API. A chave de API nunca fica exposta no navegador: o Alpine.js chama seu próprio servidor, que encaminha a requisição à CPFHub.io com o header x-api-key. Veja a documentação oficial do Alpine.js para referência das diretivas usadas neste guia.


1. Pré-requisitos

  • Um servidor web ou backend para servir HTML e funcionar como proxy da API (a chave de API não deve ficar no frontend).

  • Conhecimento básico de HTML e JavaScript.

  • Uma conta gratuita na CPFHub.io


2. Arquitetura da solução

Em aplicações frontend, a chave de API nunca deve ficar exposta no navegador. A arquitetura recomendada utiliza um backend proxy:

[Alpine.js Frontend] --fetch--> [Backend Proxy] --API Key--> [CPFHub.io API]

O backend recebe o CPF do frontend, adiciona a chave de API e encaminha a requisição para a CPFHub.io.


3. Crie o backend proxy

Um servidor simples em Node.js (Express) para proxy da API:

// server.js
const express = require("express");
const path = require("path");
const app = express();

const CPFHUB_API_KEY = process.env.CPFHUB_API_KEY || "SUA_CHAVE_DE_API";
const CPFHUB_BASE_URL = "https://api.cpfhub.io";
const TIMEOUT_MS = 5000;

app.use(express.static("public"));
app.use(express.json());

app.get("/api/cpf/:cpf", async (req, res) => {
    const cpf = req.params.cpf.replace(/\D/g, "");

    if (cpf.length !== 11) {
    return res.status(400).json({
    success: false,
    error: "CPF deve conter exatamente 11 dígitos.",
    });
    }

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);

    try {
    const response = await fetch(`${CPFHUB_BASE_URL}/cpf/${cpf}`, {
    headers: {
    "x-api-key": CPFHUB_API_KEY,
    "Accept": "application/json",
    },
    signal: controller.signal,
    });

    clearTimeout(timeoutId);

    const data = await response.json();

    if (response.ok && data.success) {
    return res.json({ success: true, data: data.data });
    }

    const errorMap = {
    400: "CPF com formato inválido.",
    401: "Erro de autenticação no servidor.",
    404: "CPF não encontrado na base de dados.",
    };

    return res.status(response.status).json({
    success: false,
    error: errorMap[response.status] || `Erro HTTP ${response.status}`,
    });
    } catch (error) {
    clearTimeout(timeoutId);

    if (error.name === "AbortError") {
    return res.status(504).json({
    success: false,
    error: "Timeout na consulta.",
    });
    }

    return res.status(502).json({
    success: false,
    error: "Erro de conexão com a API.",
    });
    }
});

app.listen(3000, () => {
    console.log("Servidor iniciado na porta 3000");
});

4. Crie a interface com Alpine.js

O componente Alpine.js com x-data gerencia todo o estado da consulta:

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Consulta de CPF - Alpine.js</title>
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <link rel="stylesheet" href="/style.css">
</head>
<body>

<div class="container" x-data="cpfConsulta()">

    <h1>Consulta de CPF</h1>
    <p>Validacao de dados cadastrais via <strong>CPFHub.io</strong></p>

    <!-- Formulário -->
    <form @submit.prevent="consultar">
    <div class="form-group">
    <label for="cpf">CPF</label>
    <input
    type="text"
    id="cpf"
    x-model="cpfFormatado"
    @input="aplicarMascara"
    placeholder="000.000.000-00"
    maxlength="14"
    :disabled="loading"
    />
    </div>

    <button type="submit" :disabled="loading || cpfFormatado.length < 14">
    <span x-show="!loading">Consultar CPF</span>
    <span x-show="loading">Consultando...</span>
    </button>
    </form>

    <!-- Indicador de loading -->
    <div x-show="loading" x-transition class="loading">
    Consultando a API da CPFHub.io...
    </div>

    <!-- Resultado de sucesso -->
    <div x-show="resultado" x-transition class="card sucesso">
    <h3>CPF Encontrado</h3>
    <table>
    <tr>
    <td><strong>Nome</strong></td>
    <td x-text="resultado?.name"></td>
    </tr>
    <tr>
    <td><strong>CPF</strong></td>
    <td x-text="resultado?.cpf"></td>
    </tr>
    <tr>
    <td><strong>Genero</strong></td>
    <td x-text="resultado?.gender === 'M' ? 'Masculino' : 'Feminino'"></td>
    </tr>
    <tr>
    <td><strong>Nascimento</strong></td>
    <td x-text="resultado?.birthDate"></td>
    </tr>
    <tr>
    <td><strong>Idade</strong></td>
    <td x-text="calcularIdade(resultado?.year)"></td>
    </tr>
    </table>
    </div>

    <!-- Mensagem de erro -->
    <div x-show="erro" x-transition class="card erro">
    <p x-text="erro"></p>
    </div>

</div>

<script>
function cpfConsulta() {
    return {
    cpfFormatado: "",
    resultado: null,
    erro: null,
    loading: false,

    aplicarMascara() {
    let numeros = this.cpfFormatado.replace(/\D/g, "").slice(0, 11);

    if (numeros.length > 9) {
    this.cpfFormatado = `${numeros.slice(0, 3)}.${numeros.slice(3, 6)}.${numeros.slice(6, 9)}-${numeros.slice(9)}`;
    } else if (numeros.length > 6) {
    this.cpfFormatado = `${numeros.slice(0, 3)}.${numeros.slice(3, 6)}.${numeros.slice(6)}`;
    } else if (numeros.length > 3) {
    this.cpfFormatado = `${numeros.slice(0, 3)}.${numeros.slice(3)}`;
    } else {
    this.cpfFormatado = numeros;
    }
    },

    async consultar() {
    const cpf = this.cpfFormatado.replace(/\D/g, "");

    if (cpf.length !== 11) {
    this.erro = "CPF deve conter 11 digitos.";
    this.resultado = null;
    return;
    }

    this.loading = true;
    this.resultado = null;
    this.erro = null;

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 5000);

    try {
    const response = await fetch(`/api/cpf/${cpf}`, {
    headers: { "Accept": "application/json" },
    signal: controller.signal,
    });

    clearTimeout(timeoutId);

    const data = await response.json();

    if (data.success) {
    this.resultado = data.data;
    } else {
    this.erro = data.error || "Erro na consulta.";
    }
    } catch (error) {
    clearTimeout(timeoutId);

    if (error.name === "AbortError") {
    this.erro = "Timeout na consulta. Tente novamente.";
    } else {
    this.erro = "Erro de conexao. Verifique sua internet.";
    }
    } finally {
    this.loading = false;
    }
    },

    calcularIdade(anoNascimento) {
    if (!anoNascimento) return "";
    const idade = new Date().getFullYear() - anoNascimento;
    return `${idade} anos`;
    },
    };
}
</script>

</body>
</html>

5. Adicione estilos CSS

/* public/style.css */
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: #f5f5f5;
    color: #333;
    line-height: 1.6;
}

.container {
    max-width: 560px;
    margin: 40px auto;
    padding: 0 20px;
}

h1 { margin-bottom: 4px; }
p { margin-bottom: 24px; color: #666; }

.form-group { margin-bottom: 16px; }

label {
    display: block;
    margin-bottom: 4px;
    font-weight: 600;
}

input[type="text"] {
    width: 100%;
    padding: 12px;
    font-size: 18px;
    border: 2px solid #ddd;
    border-radius: 6px;
    transition: border-color 0.2s;
}

input:focus { outline: none; border-color: #3498db; }
input:disabled { background: #eee; cursor: not-allowed; }

button {
    width: 100%;
    padding: 14px;
    font-size: 16px;
    font-weight: 600;
    background: #3498db;
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    transition: background 0.2s;
}

button:hover:not(:disabled) { background: #2980b9; }
button:disabled { opacity: 0.6; cursor: not-allowed; }

.loading {
    margin-top: 16px;
    padding: 12px;
    text-align: center;
    color: #888;
    font-style: italic;
}

.card {
    margin-top: 24px;
    padding: 20px;
    border-radius: 8px;
    border: 1px solid #ddd;
}

.card.sucesso { border-color: #27ae60; background: #f0fff4; }
.card.erro { border-color: #e74c3c; background: #fff5f5; color: #c0392b; }

table { width: 100%; border-collapse: collapse; margin-top: 12px; }
td { padding: 8px 0; border-bottom: 1px solid #eee; }
td:first-child { width: 120px; color: #555; }

6. Adicione consulta com debounce

Para consultar automaticamente enquanto o usuário digita, com debounce:

<!-- Adicione ao input -->
<input
    type="text"
    x-model="cpfFormatado"
    @input="aplicarMascara"
    @input.debounce.500ms="consultarSeCompleto"
    placeholder="000.000.000-00"
    maxlength="14"
/>
// Adicione ao objeto x-data
consultarSeCompleto() {
    const cpf = this.cpfFormatado.replace(/\D/g, "");
    if (cpf.length === 11) {
    this.consultar();
    }
},

7. Adicione histórico de consultas

Use x-data para manter um histórico local das consultas:

<!-- Histórico de consultas -->
<div x-show="historico.length > 0" class="historico">
    <h3>Historico de Consultas</h3>
    <template x-for="item in historico" :key="item.cpf">
    <div class="historico-item" @click="preencherCpf(item.cpf)">
    <span x-text="item.nome"></span>
    <small x-text="item.cpf"></small>
    </div>
    </template>
    <button @click="limparHistorico" class="btn-limpar">Limpar Historico</button>
</div>
// Adicione ao objeto x-data
historico: JSON.parse(localStorage.getItem("cpf_historico") || "[]"),

salvarNoHistorico(dados) {
    const item = { cpf: dados.cpf, nome: dados.name, data: new Date().toISOString() };
    this.historico = [item, ...this.historico.filter(h => h.cpf !== dados.cpf)].slice(0, 10);
    localStorage.setItem("cpf_historico", JSON.stringify(this.historico));
},

preencherCpf(cpf) {
    const numeros = cpf.replace(/\D/g, "");
    this.cpfFormatado = `${numeros.slice(0, 3)}.${numeros.slice(3, 6)}.${numeros.slice(6, 9)}-${numeros.slice(9)}`;
    this.consultar();
},

limparHistorico() {
    this.historico = [];
    localStorage.removeItem("cpf_historico");
},

8. Boas práticas

  • Backend proxy -- Nunca exponha a chave de API no frontend. O Alpine.js faz requisições ao seu backend, que adiciona a chave e consulta a CPFHub.io.

  • Timeout -- Use AbortController com timeout de 5 segundos para evitar que a interface trave em conexões lentas.

  • Debounce -- Use @input.debounce.500ms para evitar consultas excessivas enquanto o usuário digita.

  • Transições -- Use x-transition para animar a exibição de resultados e erros, melhorando a experiência do usuário.

  • LocalStorage -- Para o histórico, use localStorage com cuidado. Dados de CPF são sensíveis e devem ser tratados conforme a LGPD.

  • LGPD -- A API da CPFHub.io é 100% compatível com a LGPD. No frontend, não armazene dados pessoais sem consentimento explícito do usuário.


Perguntas frequentes

Por que usar um backend proxy ao integrar Alpine.js com a API de CPF?

O Alpine.js roda no navegador, o que significa que qualquer dado incluído no código JavaScript fica visível para o usuário. Colocar a chave de API diretamente no frontend a expõe a qualquer pessoa que inspecione o código-fonte. O backend proxy recebe o CPF, adiciona a chave de forma segura e repassa a requisição à CPFHub.io — protegendo suas credenciais e limitando o acesso ao endpoint.

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

Não. Ao atingir o limite de 50 consultas mensais do plano gratuito, a API continua respondendo e cobra R$0,15 por consulta adicional, sem bloquear nem retornar erro de cota. Para volumes previsíveis e maiores, o plano Pro oferece 1.000 consultas por R$149/mês com o mesmo excedente de R$0,15/consulta.

Como o Alpine.js lida com a latência de ~900ms da API de CPF?

O Alpine.js permite gerenciar o estado de loading com x-show="loading" e x-transition, exibindo uma mensagem de "consultando" enquanto a resposta chega. O debounce de 500ms no input evita disparar múltiplas requisições durante a digitação, e o AbortController cancela a chamada caso o usuário desista antes da resposta.

O histórico de consultas em localStorage é seguro para dados de CPF?

O localStorage persiste dados no navegador do usuário e pode ser lido por qualquer script da mesma origem. Para conformidade com a LGPD, armazene apenas o mínimo necessário (por exemplo, apenas os últimos 4 dígitos do CPF para identificação visual), obtenha consentimento explícito do usuário e disponibilize uma opção clara para limpar o histórico.



Conclusão

Consumir a API da 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