Gerador de relatórios para Next.js

Next.js (server)

Um route handler do App Router recebe a definição do relatório e as linhas, escolhe o formato pela querystring e devolve uma Response pronta — sem tocar em Content-Type, Content-Disposition ou nos bytes de cada exporter. A rota abaixo é real: os botões chamam exatamente ela.

Teste agora — direto deste site

Cada botão abre /api/exemplo-vendas numa aba nova, com format na querystring. É a mesma rota GET que gera o relatório de vendas (36 linhas) nos quatro formatos suportados.

app/api/exemplo-vendas/route.ts

O arquivo completo — importa relatorioVendas e vendas de lib/demo-data, valida ?format= e delega tudo para renderReportResponse.

ts
import type { NextRequest } from "next/server";
import { renderReportResponse } from "cosmemilton-report/next";
import type { ReportOutputFormat } from "cosmemilton-report";
import { relatorioVendas, vendas } from "@/lib/demo-data";

/** Formatos que esta rota aceita — subconjunto de ReportOutputFormat (sem "tsv"). */
const FORMATOS_SUPORTADOS = ["pdf", "csv", "xlsx", "json"] as const satisfies readonly ReportOutputFormat[];

type FormatoSuportado = (typeof FORMATOS_SUPORTADOS)[number];

function isFormatoSuportado(value: string): value is FormatoSuportado {
  return (FORMATOS_SUPORTADOS as readonly string[]).includes(value);
}

/**
 * Exemplo real de rota server-side: gera o relatório de vendas no formato pedido
 * via ?format= (pdf | csv | xlsx | json, default "pdf") e devolve a Response já
 * pronta — Content-Type e Content-Disposition inclusos — via renderReportResponse.
 */
export async function GET(request: NextRequest) {
  const formatoParam = request.nextUrl.searchParams.get("format") ?? "pdf";

  if (!isFormatoSuportado(formatoParam)) {
    return Response.json(
      {
        error: `Formato inválido: "${formatoParam}". Use um de: ${FORMATOS_SUPORTADOS.join(", ")}.`,
      },
      { status: 400 },
    );
  }

  return renderReportResponse({
    definition: relatorioVendas,
    rows: vendas,
    format: formatoParam,
    fileName: "exemplo-vendas",
    globalConfig: { companyName: "Cosmemilton Demo" },
  });
}

Por que server-side?

Gerar o export no servidor — em vez de montar o PDF/planilha inteiro no navegador — traz quatro ganhos diretos.

Payload menor: o navegador baixa só os bytes finais do PDF/planilha, não os dados brutos para montar tudo no cliente.

Streaming: reportResponse aceita um ReadableStream como corpo — o download pode começar antes do relatório inteiro estar pronto.

Bundle menor no cliente: @react-pdf/renderer e as bibliotecas de planilha ficam só no servidor, sem inflar o JavaScript enviado ao navegador.

Segredos protegidos: a busca dos dados (banco, chaves de API) roda no servidor e nunca é exposta no navegador.

Server action (alternativa)

Sem um route handler dedicado, uma server action também consegue gerar o PDF no servidor — mas com um custo de payload e sem os headers HTTP prontos.

tsx
"use server";

import { renderReportToBuffer } from "cosmemilton-report/pdf";
import { relatorioVendas, vendas } from "@/lib/demo-data";

export async function baixarVendasPdfBase64() {
  const bytes = await renderReportToBuffer({ definition: relatorioVendas, rows: vendas });
  // Server actions só devolvem tipos serializáveis — não um Uint8Array/Response,
  // por isso os bytes viram base64 aqui e um Blob de volta no cliente.
  return Buffer.from(bytes).toString("base64");
}

// No client component que chama a action:
// const base64 = await baixarVendasPdfBase64();
// const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
// const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
// window.open(url, "_blank");

Ressalvas

Base64 infla o payload em ~33% — para arquivos grandes, a rota GET é mais leve.

Sem streaming: a action só responde depois que o PDF inteiro está pronto e serializado.

Sem Content-Disposition/Cache-Control: quem monta o Blob e dispara o download é o cliente.

Ver guia de PDF