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

**Publicado:** 25/07/2026
**Autor:** Redação CPFHub.io
**URL:** https://cpfhub.io/blog/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](https://alpinejs.dev) 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**](https://www.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:

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

```html
<!-- 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

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

```html
<!-- Adicione ao input -->
<input
 type="text"
 x-model="cpfFormatado"
 @input="aplicarMascara"
 @input.debounce.500ms="consultarSeCompleto"
 placeholder="000.000.000-00"
 maxlength="14"
/>
```

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

```html
<!-- 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>
```

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

---

### 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 TypeScript com tipagem segura](https://cpfhub.io/blog/como-consumir-api-de-cpf-em-typescript-com-tipagem-segura)
- [Como integrar validação de CPF em Stimulus (Hotwire/Rails) com controllers](https://cpfhub.io/blog/como-integrar-validacao-de-cpf-em-stimulus-hotwire-rails-com-controllers)

---

## Conclusão

Consumir a API da [**CPFHub.io**](https://www.cpfhub.io/)

Cadastre-se em [cpfhub.io](https://www.cpfhub.io/)

