DEV Community

CPFHub
CPFHub

Posted on

Consulta de CPF no Next.js sem vazar a API key: Route Handler, Server Action e cache

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_
Enter fullscreen mode Exit fullscreen mode

Para garantir que um módulo nunca seja importado no cliente:

// lib/cpf.server.ts
import "server-only";
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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] });
}
Enter fullscreen mode Exit fullscreen mode

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] };
}
Enter fullscreen mode Exit fullscreen mode
"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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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)