csv-parser
csv-parser is a dependency-free CommonJS Transform stream that turns CSV bytes into one JavaScript object per row. It handles quoted fields, embedded newlines, custom separators, header and value mapping, strict column counts, row byte offsets, and backpressure through Node streams. It also installs a command named csv-parser that converts a file or standard input to newline-delimited JSON. The parser is fast and focused, but deliberately leaves encoding conversion, BOM removal, schema validation, and full-table collection to other code.
One of the best small Node choices for fast, backpressure-aware CSV ingestion. Set strict and maxRowBytes deliberately, clean up encoding first, and avoid the currently nonfunctional CLI flags.
Use it if
- You need to process a large CSV incrementally without loading the whole file into memory
- Your Node pipeline already uses readable, transform, and writable streams
- You want header normalization and lightweight value conversion while each row is parsed
- You need byte offsets so failed or indexed rows can be traced back to the source stream
- You need a Promise that returns the whole table: this package exposes a stream, and manually pushing every row into an array gives up its memory advantage
- You need browser-native ESM: the package is CommonJS and the README's browser route is Browserify rather than a modern browser build
- Your input can contain huge hostile rows and you will not set maxRowBytes: the documented default is Number.MAX_SAFE_INTEGER, so the safety limit is effectively absent
- You need automatic delimiter detection, typed schema validation, nested output, or detailed recovery diagnostics: the API accepts one single-byte separator and emits a generic row-length error in strict mode
- You rely on the documented CLI flags --strict or --remove: version 3.2.1 advertises them in help, but its argument parser has no cases that apply either option
Setup reality
`npm install csv-parser` gives you a CommonJS parser, TypeScript declarations, and the csv-parser executable. It requires Node 10 or newer according to package.json, although the README still says Node 8.16, so trust the package engine field. The happy path is a file stream piped into csv(), but production setup needs limits and input cleanup. Values are strings unless mapValues converts them. The first row becomes headers unless you pass headers, and supplying headers does not consume an existing header row, so add skipLines: 1 when both are present. strict defaults to false; short rows can silently shift meaning around missing middle columns, while long rows are placed under keys such as _10. Set strict: true when column count is contractual. maxRowBytes defaults to Number.MAX_SAFE_INTEGER and should be capped for untrusted files. UTF-8 BOMs are not removed, and non-UTF-8 input needs a transcoding stream such as iconv-lite before the parser. Separator, quote, escape, and custom newline options are reduced to their first byte, so multi-character or multibyte delimiters do not work. Version 3.2.1 blocks prototype-pollution keys by dropping columns named __proto__, constructor, or prototype, which is safer but can surprise consumers of legitimate files with those headers. Always handle pipeline errors and consume rows with a pipeline, async iterator, or writable stream so backpressure remains effective. The CLI emits NDJSON, not a JSON array, and its current --strict and --remove help entries do not match the argument parser implementation.
Patterns
Parse a CSV file row by rowparse-csv-file
const fs = require('node:fs');
const csv = require('csv-parser');
fs.createReadStream('data.csv')
.pipe(csv())
.on('data', (row) => console.log(row))
.on('error', (error) => console.error(error))
.on('end', () => console.log('done'));Every value is a string by default, and the first row is consumed as headers rather than emitted as data.
Consume rows with an async iteratorconsume-with-async-iterator
const input = fs.createReadStream('data.csv').pipe(csv());
for await (const row of input) {
await saveRow(row);
}Sequential awaiting preserves backpressure. An error from the file or parser rejects the loop.
Use numeric keys when the file has no headersparse-without-header-row
fs.createReadStream('rows.csv')
.pipe(csv({ headers: false }))
.on('data', (row) => {
console.log(row['0'], row['1']);
});headers: false also disables strict mode internally, so validate the numeric columns yourself.
Supply headers and skip the file's header linesupply-known-headers
const parser = csv({
headers: ['name', 'age'],
skipLines: 1,
strict: true,
});
fs.createReadStream('people.csv').pipe(parser);Without skipLines: 1, the original header row is emitted as ordinary data under the supplied names.
Normalize headers and drop a columnnormalize-and-drop-headers
const parser = csv({
mapHeaders: ({ header }) => {
const key = header.trim().toLowerCase();
return key === 'internal_notes' ? null : key;
},
});Returning null removes both the header and every value in that column. Version 3.2.1 also drops __proto__, constructor, and prototype headers.
Convert selected values while streamingconvert-column-values
const parser = csv({
mapValues: ({ header, value }) => {
if (header === 'age') return Number(value);
if (header === 'active') return value === 'true';
return value;
},
});Number('') is 0 and invalid numbers become NaN. Add explicit validation when empty and malformed cells have different meanings.
Reject mismatched or oversized rowslimit-untrusted-rows
const { pipeline } = require('node:stream');
const parser = csv({
strict: true,
maxRowBytes: 1024 * 1024, // 1 MiB
});
pipeline(
fs.createReadStream('upload.csv'),
parser,
destination,
(error) => { if (error) reportImportFailure(error); }
);The default row limit is Number.MAX_SAFE_INTEGER. strict catches column-count mismatches but does not validate types or required values.
Parse tab-separated dataparse-tsv
fs.createReadStream('data.tsv')
.pipe(csv({ separator: '\t' }))
.on('data', processRow);The separator is one byte. Multi-character separators and multibyte Unicode delimiter characters are not supported.
Skip a preamble and comment linesskip-comments-and-preamble
const parser = csv({
skipLines: 2,
skipComments: '#',
});
fs.createReadStream('export.csv').pipe(parser);CSV comments are non-standard. A comment is recognized only when its marker begins the physical line.
Remove a UTF-8 BOM before parsingstrip-utf8-bom
const stripBom = require('strip-bom-stream');
fs.createReadStream('excel-export.csv')
.pipe(stripBom())
.pipe(csv())
.on('data', processRow);csv-parser does not remove a BOM itself; without this step the first header can contain an invisible leading character.
Track each row's source byte offsetemit-row-byte-offsets
fs.createReadStream('data.csv')
.pipe(csv({ outputByteOffset: true }))
.on('data', ({ byteOffset, row }) => {
indexRow(byteOffset, row);
});This option changes every emitted chunk from a row object to `{ byteOffset, row }`; update TypeScript and downstream transforms accordingly.
Convert CSV to NDJSON with the CLIconvert-to-ndjson
npx csv-parser input.csv > output.ndjson
# or from stdin
node produce-csv.js | npx csv-parser -s ';' > output.ndjsonThe output is one JSON object per line, not a JSON array. In 3.2.1, avoid the advertised --strict and --remove flags because the CLI parser does not implement them.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| csv-parse | npm | Use it for a larger option set, sync and callback APIs, casting controls, and richer CSV dialect handling |
| @fast-csv/parse | npm | Use it when you want TypeScript-first stream parsing with validation and row transformation hooks |
| papaparse | npm | Use it when browser support, delimiter detection, worker parsing, or browser file inputs matter |
| neat-csv | npm | Use it for small inputs when a Promise returning all parsed rows is more convenient than streaming |