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:

  1. 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).
  2. Minimização. O front raramente precisa de data de nascimento completa. Devolva só o necessário.
  3. 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.