Transparência: sou fundador da CPFHub, usada como exemplo de provedor. O padrão serve para qualquer API paga que você chame do Next.js.
Um erro comum em projetos Next.js: chamar a API de consulta de CPF direto do componente cliente com NEXT_PUBLIC_API_KEY. Qualquer variável NEXT_PUBLIC_ vai parar no bundle do navegador, e sua chave passa a ser de todo mundo. Vamos fazer do jeito certo, no App Router.
1. A chave fica no servidor
# .env.local
CPF_API_KEY=sua_chave_aqui # sem NEXT_PUBLIC_
Para garantir que um módulo nunca seja importado no cliente:
// lib/cpf.server.ts
import "server-only";
Se alguém importar esse arquivo num componente "use client", o build quebra. É uma rede de segurança barata.
2. A função de consulta
// lib/cpf.server.ts
import "server-only";
export function normalizar(cpf: string) {
return cpf.replace(/\D/g, "");
}
export function cpfValido(d: string) {
if (!/^\d{11}$/.test(d) || /^(\d)\1{10}$/.test(d)) return false;
for (let t = 9; t < 11; t++) {
let s = 0;
for (let i = 0; i < t; i++) s += Number(d[i]) * (t + 1 - i);
if (Number(d[t]) !== ((s * 10) % 11) % 10) return false;
}
return true;
}
type Resultado =
| { ok: true; nome: string; nascimento: string }
| { ok: false; motivo: "invalido" | "nao_encontrado" | "limite" | "erro" };
export async function consultarCpf(entrada: string): Promise<Resultado> {
const cpf = normalizar(entrada);
if (!cpfValido(cpf)) return { ok: false, motivo: "invalido" };
const res = await fetch(`https://api.cpfhub.io/cpf/${cpf}`, {
headers: { "x-api-key": process.env.CPF_API_KEY! },
cache: "no-store",
signal: AbortSignal.timeout(10_000),
});
if (res.status === 404) return { ok: false, motivo: "nao_encontrado" };
if (res.status === 429) return { ok: false, motivo: "limite" };
if (!res.ok) return { ok: false, motivo: "erro" };
const { data } = await res.json();
return { ok: true, nome: data.name, nascimento: data.birthDate };
}
cache: "no-store" é importante: dado pessoal não deve parar no Data Cache do Next, que é compartilhado entre usuários.
3. Route Handler
// app/api/cpf/route.ts
import { NextResponse } from "next/server";
import { consultarCpf } from "@/lib/cpf.server";
import { auth } from "@/lib/auth";
export async function POST(req: Request) {
const session = await auth();
if (!session) return NextResponse.json({ erro: "não autenticado" }, { status: 401 });
const { cpf } = await req.json();
const r = await consultarCpf(String(cpf ?? ""));
if (!r.ok) return NextResponse.json(r, { status: r.motivo === "limite" ? 429 : 422 });
// devolva o mínimo: só o primeiro nome para confirmação
return NextResponse.json({ ok: true, primeiroNome: r.nome.split(" ")[0] });
}
Três cuidados:
- Autenticação ou rate limit próprio. Um endpoint público que recebe CPF e devolve nome vira um serviço de consulta grátis para terceiros, pago por você. No mínimo, limite por IP/sessão (Upstash Ratelimit, middleware, ou o que sua infra oferecer).
- Minimização. O front raramente precisa de data de nascimento completa. Devolva só o necessário.
- Use POST, não GET. CPF em query string acaba em log de proxy, CDN e histórico do navegador.
4. Server Action
Se o CPF faz parte de um formulário, a Server Action é ainda mais simples:
// app/cadastro/actions.ts
"use server";
import { consultarCpf } from "@/lib/cpf.server";
export async function verificarCpf(_: unknown, form: FormData) {
const r = await consultarCpf(String(form.get("cpf")));
if (!r.ok) return { erro: r.motivo };
return { primeiroNome: r.nome.split(" ")[0] };
}
"use client";
import { useActionState } from "react";
import { verificarCpf } from "./actions";
export function Form() {
const [state, action, pending] = useActionState(verificarCpf, null);
return (
<form action={action}>
<input name="cpf" inputMode="numeric" />
<button disabled={pending}>Verificar</button>
{state?.primeiroNome && <p>Você é {state.primeiroNome}?</p>}
{state?.erro && <p role="alert">Não foi possível verificar ({state.erro}).</p>}
</form>
);
}
5. Cache por usuário, não global
Se o mesmo usuário pode reenviar o formulário várias vezes, guarde o resultado da verificação no registro dele no banco (verificado em X, nome conferido sim/não), em vez de cachear a resposta HTTP. Assim você não paga duas vezes nem expõe dados entre sessões.
6. Edge ou Node runtime?
A consulta é uma chamada HTTP simples e funciona nos dois. Se o resto da rota usa bibliotecas Node (ORM, SDKs), fique no runtime Node.
Sobre a CPFHub
No exemplo usei a CPFHub: GET /cpf/{cpf} com x-api-key, ~150 ms típicos (não SLA), 404 sem consumo de crédito, limite padrão de 30 requisições/min (429 com Retry-After) e 50 créditos grátis por mês. Veja a documentação.
Top comments (0)