PapaParse is the fastest in-browser and Node.js CSV parser — Papa.parse(csvString, { header: true, dynamicTyping: true }) parses CSV to typed objects using the first row as keys. Papa.parse(file, { step: (row) => process(row) }) streams a File object row-by-row without loading it all into memory. Papa.unparse(data, { header: true }) converts arrays or objects back to CSV strings. header: true maps columns to object properties. dynamicTyping: true converts numeric strings to numbers and “true”/“false” to booleans. worker: true parses in a Web Worker to avoid blocking the main thread. skipEmptyLines: true ignores blank rows. transformHeader: (h) => h.toLowerCase().replace(/\s+/g, "_") normalizes column names. complete, error, and chunk callbacks handle async streaming results. Claude Code generates PapaParse parsers, typed importers, CSV exporters, streaming processors, and browser file-input upload handlers.
CLAUDE.md for PapaParse
## PapaParse Stack
- Version: papaparse >= 5.4, @types/papaparse >= 5.3
- Parse string: Papa.parse<Row>(csv, { header: true, dynamicTyping: true, skipEmptyLines: true })
- Parse file: Papa.parse(file, { header: true, step: (results) => process(results.data), complete })
- Stream large: Papa.parse(readable, { step, complete, worker: false }) — Node.js ReadableStream
- Unparse: Papa.unparse(data, { header: true, columns: ["id", "name"] })
- Type: result.data — typed as Row[]; result.errors — meta.fields gets detected headers
- Transform: transformHeader: h => h.trim().toLowerCase().replace(/\s+/g, "_")
- Node: import Papa from "papaparse" — works in Node.js with same API
Browser CSV Import
// components/import/CsvImporter.tsx — browser file upload with preview
"use client"
import { useState, useCallback } from "react"
import Papa from "papaparse"
type ImportRow = Record<string, string | number | boolean | null>
type ImportResult = {
rows: ImportRow[]
headers: string[]
errors: Papa.ParseError[]
totalRows: number
}
interface CsvImporterProps {
expectedColumns?: string[]
onImport: (rows: ImportRow[]) => Promise<void>
maxRows?: number
}
export function CsvImporter({ expectedColumns, onImport, maxRows = 10_000 }: CsvImporterProps) {
const [preview, setPreview] = useState<ImportResult | null>(null)
const [isParsing, setIsParsing] = useState(false)
const [isImporting, setIsImporting] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleFile = useCallback((file: File) => {
setError(null)
setIsParsing(true)
// For large files — stream with step callback
if (file.size > 5 * 1024 * 1024) { // > 5MB
const rows: ImportRow[] = []
let headers: string[] = []
Papa.parse<ImportRow>(file, {
header: true,
dynamicTyping: true,
skipEmptyLines: "greedy",
transformHeader: h => h.trim().toLowerCase().replace(/\s+/g, "_"),
step: (result) => {
if (rows.length === 0 && result.meta.fields) {
headers = result.meta.fields
}
if (rows.length < 5) rows.push(result.data) // Preview first 5 rows
},
complete: (results) => {
setPreview({
rows: rows.slice(0, 5),
headers,
errors: results.errors.slice(0, 3),
totalRows: results.data.length,
})
setIsParsing(false)
},
error: (err) => {
setError(err.message)
setIsParsing(false)
},
})
} else {
// Small files — parse all at once
Papa.parse<ImportRow>(file, {
header: true,
dynamicTyping: true,
skipEmptyLines: "greedy",
transformHeader: h => h.trim().toLowerCase().replace(/\s+/g, "_"),
complete: (results) => {
setPreview({
rows: results.data.slice(0, 5), // Preview first 5
headers: results.meta.fields ?? [],
errors: results.errors.slice(0, 3),
totalRows: results.data.length,
})
setIsParsing(false)
},
error: (err) => {
setError(err.message)
setIsParsing(false)
},
})
}
}, [])
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
const file = e.dataTransfer.files[0]
if (file?.name.endsWith(".csv") || file?.type === "text/csv") {
handleFile(file)
} else {
setError("Please drop a CSV file")
}
}, [handleFile])
const handleImport = async () => {
if (!preview) return
setIsImporting(true)
// Re-parse full file for import
const input = document.querySelector<HTMLInputElement>('#csv-file')?.files?.[0]
if (!input) return
Papa.parse<ImportRow>(input, {
header: true,
dynamicTyping: true,
skipEmptyLines: "greedy",
transformHeader: h => h.trim().toLowerCase().replace(/\s+/g, "_"),
complete: async (results) => {
try {
await onImport(results.data.slice(0, maxRows))
} catch (err) {
setError(err instanceof Error ? err.message : "Import failed")
} finally {
setIsImporting(false)
}
},
})
}
return (
<div className="space-y-4">
<div
onDrop={handleDrop}
onDragOver={e => e.preventDefault()}
className="border-2 border-dashed border-muted-foreground/30 rounded-xl p-8 text-center"
>
<label htmlFor="csv-file" className="cursor-pointer">
<p className="font-medium">Drop CSV file here</p>
<p className="text-sm text-muted-foreground mt-1">or click to browse</p>
{expectedColumns && (
<p className="text-xs text-muted-foreground mt-2">
Expected columns: {expectedColumns.join(", ")}
</p>
)}
<input
id="csv-file"
type="file"
accept=".csv,text/csv"
className="hidden"
onChange={e => e.target.files?.[0] && handleFile(e.target.files[0])}
/>
</label>
</div>
{isParsing && <p className="text-sm text-muted-foreground">Parsing...</p>}
{error && <p className="text-sm text-red-500">{error}</p>}
{preview && (
<div className="space-y-3">
<div className="flex items-center gap-4">
<p className="text-sm">
<strong>{preview.totalRows.toLocaleString()}</strong> rows detected
</p>
{preview.errors.length > 0 && (
<p className="text-sm text-yellow-600">{preview.errors.length} parse warnings</p>
)}
</div>
{/* Column validation */}
{expectedColumns && (
<div className="flex flex-wrap gap-1">
{expectedColumns.map(col => (
<span
key={col}
className={`text-xs px-2 py-0.5 rounded-full ${
preview.headers.includes(col)
? "bg-green-100 text-green-700"
: "bg-red-100 text-red-700"
}`}
>
{col} {preview.headers.includes(col) ? "✓" : "✗"}
</span>
))}
</div>
)}
{/* Preview table */}
<div className="overflow-x-auto rounded border">
<table className="text-xs w-full">
<thead>
<tr className="bg-muted">
{preview.headers.map(h => (
<th key={h} className="px-3 py-2 text-left font-medium">{h}</th>
))}
</tr>
</thead>
<tbody>
{preview.rows.map((row, i) => (
<tr key={i} className="border-t">
{preview.headers.map(h => (
<td key={h} className="px-3 py-1.5 max-w-[160px] truncate">{String(row[h] ?? "")}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<button
onClick={handleImport}
disabled={isImporting}
className="btn-primary"
>
{isImporting ? "Importing..." : `Import ${Math.min(preview.totalRows, maxRows).toLocaleString()} rows`}
</button>
</div>
)}
</div>
)
}
CSV Export Utilities
// lib/csv/export.ts — generate CSV downloads
import Papa from "papaparse"
// Generic CSV export
export function exportToCSV<T extends Record<string, unknown>>(
data: T[],
options: {
filename?: string
columns?: (keyof T)[]
headers?: Record<keyof T, string>
} = {},
): void {
const { filename = "export.csv", columns, headers } = options
// Map column names
let exportData = data
if (headers) {
exportData = data.map(row =>
Object.fromEntries(
Object.entries(row).map(([k, v]) => [headers[k as keyof T] ?? k, v]),
) as T,
)
}
// Filter columns
const csvData = columns
? exportData.map(row => Object.fromEntries(columns.map(c => [c, row[c]])))
: exportData
const csv = Papa.unparse(csvData, {
header: true,
quotes: true, // Quote all fields for safety
newline: "\r\n", // Windows line endings for Excel compatibility
})
// Trigger browser download with BOM for Excel UTF-8 support
const BOM = "\uFEFF"
const blob = new Blob([BOM + csv], { type: "text/csv;charset=utf-8" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}
// Server-side: generate CSV buffer for API response
export function generateCSVBuffer<T extends Record<string, unknown>>(data: T[]): Buffer {
const csv = Papa.unparse(data, { header: true, newline: "\r\n" })
return Buffer.from("\uFEFF" + csv, "utf-8") // BOM prefix for Excel
}
For the csv-parse (Node.js streams) alternative when large CSV files need to be processed server-side as Node.js readable/transform streams — csv-parse from the csv package suite has better streaming and async iterator support for server-side ETL pipelines than PapaParse, see the Node.js data pipeline guide. For the d3-dsv alternative when lightweight, dependency-free TSV/DSV parsing with the same CSV format already used by D3 charts is needed — d3-dsv is tiny (2KB) and suits browser-side parsing in data visualization apps where D3 is already a dependency, see the D3 data parsing guide. The Claude Skills 360 bundle includes PapaParse skill sets covering CSV import, streaming, and export. Start with the free tier to try CSV processing generation.