A lightweight, zero-dependency CSV library built for Bun (v1.4.4+).
Handles RFC 4180-compliant parsing and writing, including multiline cells with
newlines, embedded double quotes (""), custom delimiters, UTF-8 BOM, and
chunk-by-chunk file streaming via native Web Streams.
| Resource | Description |
|---|---|
| Developer Guide | Deep-dive guides covering parsing, headers, stringifying, Web Streams, file I/O, and recipes. |
| Hands-On Tutorial | Progressive 10-lesson course from first parse to building a streaming ETL pipeline. |
| API Reference | Full TypeDoc HTML reference generated from JSDoc contracts (bun run docs). |
- Zero Third-Party Dependencies: Uses only Bun's built-in APIs and standard Web Streams.
- Streaming State Machine: Processes chunks of arbitrary sizes (even 1-byte chunks) without buffering full files in memory.
- RFC 4180 Compliance: Correctly escapes embedded quotes (
""->"), multiline cells containing\r\nor\n, commas, and whitespace. - Record Mapping: Easily transform rows into typed JavaScript objects
(
{ columns: true }). - High-Level File Helpers:
readCsvFile,writeCsvFile,appendCsvFile, andstreamCsvFilepowered byBun.file. - Web Streams: Native
TransformStreamimplementations for parsing and stringifying. - Full TypeScript Support: Comprehensive types and strict type safety out of the box.
bun testimport { parseCsv, stringifyCsv } from './index.ts';
// Parse CSV text into 2D array of rows
const rows = parseCsv('name,score\nAlice,95\nBob,88');
// [ ['name', 'score'], ['Alice', '95'], ['Bob', '88'] ]
// Parse with header mapping into objects
const records = parseCsv('name,score\nAlice,95', { columns: true });
// [ { name: 'Alice', score: '95' } ]
// Serialize array of objects back to CSV
const csv = stringifyCsv([
{ age: 30, name: 'Alice' },
{ age: 25, name: 'Bob' },
]);Stream large files row by row without buffering the entire file:
import { streamCsvFile } from './index.ts';
for await (const row of streamCsvFile('large_dataset.csv', { columns: true })) {
console.log(row.name, row.score);
}import { appendCsvFile, readCsvFile, writeCsvFile } from './index.ts';
// Write records with headers
await writeCsvFile('users.csv', [
{ city: 'Tokyo', name: 'Alice' },
{ city: 'London', name: 'Bob' },
]);
// Append new records (automatically omits duplicate header line)
await appendCsvFile('users.csv', [{ city: 'Paris', name: 'Charlie' }]);
// Read back entire file
const users = await readCsvFile('users.csv', { columns: true });import { parseCsv, stringifyCsv } from './index.ts';
const original = [
['Quote', 'Multiline Note'],
['He said "Hello!"', 'Line 1\nLine 2\nLine 3'],
];
const csv = stringifyCsv(original);
// Quote,Multiline Note
// "He said ""Hello!""","Line 1
// Line 2
// Line 3"
const parsed = parseCsv(csv);
// Perfectly roundtrips back to original data!Use standard Web Streams to pipe byte or text streams:
import { createCsvTransformStream } from './index.ts';
const response = await fetch('https://example.com/data.csv');
const transform = createCsvTransformStream({ columns: true });
const recordStream = response.body.pipeThrough(transform);
for await (const record of recordStream) {
console.log(record);
}parseCsv<T>(text: string, options?: CsvParserOptions): T[]parseCsvStream<T>(source: AsyncIterable<string | Uint8Array>, options?: CsvParserOptions): AsyncGenerator<T>createCsvParser(options?: CsvParserOptions): CsvParser
stringifyCsv(data: CsvValue[][] | Record<string, CsvValue>[], options?: CsvStringifierOptions): stringstringifyCsvStream(items: AsyncIterable<CsvValue[] | Record<string, CsvValue>>, options?: CsvStringifierOptions): AsyncGenerator<string>createCsvStringifier(options?: CsvStringifierOptions): CsvStringifier
createCsvTransformStream<T>(options?: CsvParserOptions): TransformStream<Uint8Array | string, T>createCsvFormatTransformStream(options?: CsvStringifierOptions): TransformStream<CsvValue[] | Record<string, CsvValue>, string>createCsvReader<T>(source: AsyncIterable<Uint8Array | string>, options?: CsvParserOptions): CsvReader<T>createCsvWriter(destination: string | WritableStream, options?: CsvStringifierOptions): CsvWriter
readCsvFile<T>(filePath: string, options?: CsvParserOptions): Promise<T[]>streamCsvFile<T>(filePath: string, options?: CsvParserOptions): AsyncGenerator<T>writeCsvFile(filePath: string, data: ... , options?: CsvStringifierOptions): Promise<number>appendCsvFile(filePath: string, data: ... , options?: CsvStringifierOptions): Promise<void>
Build the production ESM bundle and TypeScript declaration files:
# Clean, bundle with Bun, and emit type declarations (.d.ts) into dist/
bun run buildThe resulting dist/ directory contains:
dist/index.js: Self-contained ESM bundle with external source maps.dist/index.d.ts: TypeScript entrypoint declaration.dist/src/*.d.ts: Module declaration files.
Generate interactive HTML API documentation from JSDoc comments via TypeDoc:
# Generate HTML API reference into docs/api-reference/
bun run docs
# Watch mode for documentation development
bun run docs:watchAll formatting, linting, typechecking, and test settings are consolidated in
bunfig.toml:
# Run tests with Bun's native test runner
bun run test
# Run Bun's native linter (bun lint)
bun run lint
# Automatically fix lint errors (bun lint --fix)
bun run lint:fix
# Format code and Markdown files using Bun's native formatter (bun fmt)
bun run format
# Verify formatting without modifying files (bun fmt --check)
bun run format:check
# Run Bun's native TypeScript type checker (bun check)
bun run typecheck