Skip to main content
Back to Blog
Tutorials
3 min read
November 8, 2024

How to Implement Data Export (CSV and PDF) in Next.js

Export data as CSV and PDF downloads from your Next.js app. Client-side generation, server-side streaming, and formatted reports.

Ryel Banfield

Founder & Lead Developer

Users expect to export their data. Here is how to add CSV and PDF export to your Next.js application.

Step 1: Client-Side CSV Export

No library needed for basic CSV:

"use client";

interface ExportData {
  headers: string[];
  rows: string[][];
}

export function exportToCsv(data: ExportData, filename: string) {
  const csvContent = [
    data.headers.join(","),
    ...data.rows.map((row) =>
      row
        .map((cell) => {
          // Escape cells containing commas or quotes
          if (cell.includes(",") || cell.includes('"') || cell.includes("\n")) {
            return `"${cell.replace(/"/g, '""')}"`;
          }
          return cell;
        })
        .join(",")
    ),
  ].join("\n");

  const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = `${filename}.csv`;
  link.click();
  URL.revokeObjectURL(url);
}

// Usage in a component
export function ExportButton({ data }: { data: any[] }) {
  function handleExport() {
    exportToCsv(
      {
        headers: ["Name", "Email", "Plan", "Revenue"],
        rows: data.map((item) => [
          item.name,
          item.email,
          item.plan,
          item.revenue.toString(),
        ]),
      },
      "customers-export"
    );
  }

  return (
    <button
      onClick={handleExport}
      className="rounded-lg border px-4 py-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-800"
    >
      Export CSV
    </button>
  );
}

Step 2: Server-Side CSV Export (Large Datasets)

// app/api/export/csv/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/db";
import { customers } from "@/db/schema";

export async function GET(req: NextRequest) {
  const data = await db.select().from(customers);

  const headers = ["ID", "Name", "Email", "Plan", "Created At"];
  const rows = data.map((c) =>
    [c.id, c.name, c.email, c.plan, c.createdAt.toISOString()].join(",")
  );

  const csv = [headers.join(","), ...rows].join("\n");

  return new NextResponse(csv, {
    headers: {
      "Content-Type": "text/csv",
      "Content-Disposition": 'attachment; filename="customers.csv"',
    },
  });
}

Step 3: Streaming CSV for Large Exports

// app/api/export/csv-stream/route.ts
import { NextRequest } from "next/server";

export async function GET(req: NextRequest) {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      // Write headers
      controller.enqueue(
        encoder.encode("ID,Name,Email,Plan,Revenue\n")
      );

      // Stream rows in batches
      let offset = 0;
      const batchSize = 1000;

      while (true) {
        const batch = await db
          .select()
          .from(customers)
          .limit(batchSize)
          .offset(offset);

        if (batch.length === 0) break;

        for (const row of batch) {
          const line = `${row.id},${row.name},${row.email},${row.plan},${row.revenue}\n`;
          controller.enqueue(encoder.encode(line));
        }

        offset += batchSize;
      }

      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/csv",
      "Content-Disposition": 'attachment; filename="export.csv"',
      "Transfer-Encoding": "chunked",
    },
  });
}

Step 4: Excel Export

pnpm add xlsx
"use client";

import * as XLSX from "xlsx";

export function exportToExcel(
  data: Record<string, any>[],
  filename: string
) {
  const worksheet = XLSX.utils.json_to_sheet(data);
  const workbook = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(workbook, worksheet, "Data");

  // Auto-size columns
  const maxWidths = Object.keys(data[0] || {}).map((key) => ({
    wch: Math.max(
      key.length,
      ...data.map((row) => String(row[key] ?? "").length)
    ),
  }));
  worksheet["!cols"] = maxWidths;

  XLSX.writeFile(workbook, `${filename}.xlsx`);
}

// Usage
<button onClick={() => exportToExcel(tableData, "report")}>
  Export Excel
</button>

Step 5: Export Dropdown Component

"use client";

import { useState, useRef, useEffect } from "react";
import { exportToCsv } from "@/lib/export-csv";
import { exportToExcel } from "@/lib/export-excel";

interface ExportDropdownProps {
  data: Record<string, any>[];
  filename: string;
}

export function ExportDropdown({ data, filename }: ExportDropdownProps) {
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    function handleClick(e: MouseEvent) {
      if (ref.current && !ref.current.contains(e.target as Node)) {
        setOpen(false);
      }
    }
    document.addEventListener("mousedown", handleClick);
    return () => document.removeEventListener("mousedown", handleClick);
  }, []);

  return (
    <div ref={ref} className="relative">
      <button
        onClick={() => setOpen(!open)}
        className="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-800"
      >
        Export
        <ChevronDownIcon className="h-4 w-4" />
      </button>

      {open && (
        <div className="absolute right-0 mt-1 w-40 rounded-lg border bg-white p-1 shadow-lg dark:border-gray-700 dark:bg-gray-800">
          <button
            onClick={() => {
              exportToCsv(data, filename);
              setOpen(false);
            }}
            className="w-full rounded px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700"
          >
            CSV (.csv)
          </button>
          <button
            onClick={() => {
              exportToExcel(data, filename);
              setOpen(false);
            }}
            className="w-full rounded px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700"
          >
            Excel (.xlsx)
          </button>
          <button
            onClick={() => {
              window.open(`/api/export/pdf?type=${filename}`);
              setOpen(false);
            }}
            className="w-full rounded px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700"
          >
            PDF (.pdf)
          </button>
        </div>
      )}
    </div>
  );
}

Step 6: Export with Progress

"use client";

import { useState } from "react";

export function LargeExportButton({ endpoint }: { endpoint: string }) {
  const [exporting, setExporting] = useState(false);

  async function handleExport() {
    setExporting(true);
    try {
      const res = await fetch(endpoint);
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      const link = document.createElement("a");
      const disposition = res.headers.get("Content-Disposition");
      const filename = disposition?.match(/filename="(.+)"/)?.[1] || "export";
      link.href = url;
      link.download = filename;
      link.click();
      URL.revokeObjectURL(url);
    } finally {
      setExporting(false);
    }
  }

  return (
    <button
      onClick={handleExport}
      disabled={exporting}
      className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
    >
      {exporting ? "Generating..." : "Export Report"}
    </button>
  );
}

Need Data Export Features?

We build web applications with data export, reporting, and analytics dashboards. Contact us to discuss your project.

data exportCSVPDFNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles