SheetJS vs ExcelJS vs node-xlsx: Excel Files in Node.js (2026)
TL;DR
For Excel file handling in Node.js: SheetJS (xlsx) is the most capable and widely-used — it reads/writes nearly every spreadsheet format (XLSX, XLS, ODS, CSV) and works in both Node.js and browsers. ExcelJS has a cleaner streaming API for large files and better formatting control. node-xlsx is a lightweight wrapper for simple read/write operations. For most projects, SheetJS is the default choice.
Key Takeaways
- SheetJS (xlsx): ~7.8M weekly downloads — most feature-complete, handles 20+ file formats
- ExcelJS: ~1.9M weekly downloads — streaming support, rich cell formatting, pivot tables
- node-xlsx: ~450K weekly downloads — simple API, lightweight, XLSX only
- SheetJS Community Edition is free; SheetJS Pro adds better streaming and some formats
- ExcelJS is better for generating formatted Excel reports (rich styles, charts, conditional formatting)
- All three work in browsers (Blob/ArrayBuffer output) as well as Node.js
Download Trends
| Package | Weekly Downloads | Formats | Browser Support |
|---|---|---|---|
xlsx (SheetJS) | ~7.8M | 20+ | ✅ |
exceljs | ~1.9M | XLSX, CSV | ✅ |
node-xlsx | ~450K | XLSX | ✅ |
SheetJS
SheetJS handles virtually every spreadsheet format:
import * as XLSX from "xlsx"
import { readFileSync, writeFileSync } from "fs"
// Read an Excel file:
const workbook = XLSX.readFile("packages.xlsx")
// Get first sheet:
const sheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[sheetName]
// Convert sheet to JSON (array of objects using first row as headers):
const data: PackageRow[] = XLSX.utils.sheet_to_json(worksheet, {
header: 1, // Return arrays (default is objects using header row)
defval: null, // Default value for empty cells
blankrows: false, // Skip blank rows
raw: false, // Return formatted strings instead of raw values
})
// Or with automatic header detection:
const objects: PackageData[] = XLSX.utils.sheet_to_json(worksheet, {
defval: null,
raw: true, // Keep numbers as numbers, dates as numbers
})
// Get range of data:
const range = XLSX.utils.decode_range(worksheet["!ref"] ?? "A1")
console.log(`Rows: ${range.e.r + 1}, Cols: ${range.e.c + 1}`)
SheetJS write to Excel:
// Create from scratch:
const wb = XLSX.utils.book_new()
const packageData = [
["Package", "Weekly Downloads", "Version", "License"],
["react", 25000000, "18.2.0", "MIT"],
["vue", 7000000, "3.4.0", "MIT"],
["angular", 3500000, "17.0.0", "MIT"],
]
// From 2D array:
const ws = XLSX.utils.aoa_to_sheet(packageData)
// Or from array of objects (auto-generates headers):
const ws2 = XLSX.utils.json_to_sheet(packages, {
header: ["name", "downloads", "version", "license"],
skipHeader: false, // Include header row
})
// Column widths:
ws["!cols"] = [
{ wch: 20 }, // Column A — 20 chars wide
{ wch: 18 }, // Column B
{ wch: 10 }, // Column C
{ wch: 10 }, // Column D
]
// Append sheet to workbook:
XLSX.utils.book_append_sheet(wb, ws, "Packages")
// Write to file:
XLSX.writeFile(wb, "output.xlsx")
// Or get as buffer for API response:
const buffer = XLSX.write(wb, { type: "buffer", bookType: "xlsx" })
// Buffer can be sent as HTTP response or uploaded to S3
SheetJS in the browser (file download):
import * as XLSX from "xlsx"
function downloadExcel(data: PackageData[], filename = "packages.xlsx") {
const wb = XLSX.utils.book_new()
const ws = XLSX.utils.json_to_sheet(data)
XLSX.utils.book_append_sheet(wb, ws, "Data")
// Download in browser:
XLSX.writeFile(wb, filename)
// Or: const blob = new Blob([XLSX.write(wb, { type: "array" })], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" })
}
SheetJS cell-level access:
// Access individual cells:
const cell = worksheet["B2"]
console.log(cell.v) // Raw value (number, string, boolean, Date)
console.log(cell.t) // Type: "n"=number, "s"=string, "b"=boolean, "d"=date
console.log(cell.w) // Formatted string (how it appears in Excel)
console.log(cell.f) // Formula (if applicable)
// Modify a cell:
worksheet["B2"] = { t: "n", v: 25000000, z: "#,##0" } // Number with comma format
// Add formula:
worksheet["E2"] = { t: "n", f: "=B2/C2" }
ExcelJS
ExcelJS has a more OOP API with better streaming and rich formatting:
import ExcelJS from "exceljs"
// Create a workbook:
const workbook = new ExcelJS.Workbook()
workbook.creator = "PkgPulse"
workbook.created = new Date()
const worksheet = workbook.addWorksheet("Package Downloads", {
pageSetup: { paperSize: 9, orientation: "landscape" },
views: [{ state: "frozen", ySplit: 1 }], // Freeze header row
})
// Define columns with widths and header styles:
worksheet.columns = [
{ header: "Package", key: "name", width: 20 },
{ header: "Downloads", key: "downloads", width: 18, style: { numFmt: "#,##0" } },
{ header: "Version", key: "version", width: 10 },
{ header: "License", key: "license", width: 10 },
{ header: "Updated", key: "updatedAt", width: 14, style: { numFmt: "yyyy-mm-dd" } },
]
// Style the header row:
const headerRow = worksheet.getRow(1)
headerRow.font = { bold: true, size: 12, color: { argb: "FFFFFFFF" } }
headerRow.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FF2D6A4F" },
}
headerRow.alignment = { vertical: "middle", horizontal: "center" }
headerRow.height = 25
// Add rows:
packages.forEach((pkg, index) => {
const row = worksheet.addRow({
name: pkg.name,
downloads: pkg.downloads,
version: pkg.version,
license: pkg.license,
updatedAt: new Date(pkg.updatedAt),
})
// Zebra striping:
if (index % 2 === 0) {
row.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFF5F5F5" } }
}
// Conditional formatting: red if downloads < 1000/week:
if (pkg.downloads < 1000) {
row.getCell("downloads").font = { color: { argb: "FFFF0000" }, bold: true }
}
})
// Add auto-filter:
worksheet.autoFilter = { from: "A1", to: "E1" }
// Add a totals row:
worksheet.addRow([]) // Blank row
const totalRow = worksheet.addRow({
name: "TOTAL",
downloads: { formula: `SUM(B2:B${packages.length + 1})` },
})
totalRow.font = { bold: true }
// Write to file:
await workbook.xlsx.writeFile("packages-report.xlsx")
// Or write to buffer:
const buffer = await workbook.xlsx.writeBuffer()
ExcelJS streaming for large files:
import ExcelJS from "exceljs"
import { createReadStream, createWriteStream } from "fs"
// Stream reading (low memory):
async function* streamReadExcel(filepath: string) {
const workbook = new ExcelJS.Workbook()
const stream = createReadStream(filepath)
await workbook.xlsx.read(stream)
const worksheet = workbook.worksheets[0]
worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
if (rowNumber > 1) { // Skip header
yield row.values
}
})
}
// Stream writing (doesn't hold entire workbook in memory):
const workbookWriter = new ExcelJS.stream.xlsx.WorkbookWriter({
filename: "large-output.xlsx",
useStyles: true,
})
const worksheetWriter = workbookWriter.addWorksheet("data")
worksheetWriter.columns = [{ header: "Name", key: "name" }, { header: "Value", key: "value" }]
for await (const record of largeDataSource) {
worksheetWriter.addRow(record).commit() // Commit each row to disk immediately
}
await worksheetWriter.commit()
await workbookWriter.commit()
node-xlsx
node-xlsx is the simplest API — thin wrapper around SheetJS:
import xlsx from "node-xlsx"
import { readFileSync, writeFileSync } from "fs"
// Parse:
const workSheetsFromFile = xlsx.parse(readFileSync("packages.xlsx"))
const firstSheet = workSheetsFromFile[0]
console.log(firstSheet.name) // "Sheet1"
console.log(firstSheet.data) // 2D array: [[header1, header2], [val1, val2], ...]
// Build and write:
const data = [
["Package", "Downloads", "Version"],
["react", 25000000, "18.2.0"],
["vue", 7000000, "3.4.0"],
]
const buffer = xlsx.build([{ name: "Packages", data }])
writeFileSync("output.xlsx", buffer)
node-xlsx is perfect when you just need to read or write simple 2D data without any formatting.
Feature Comparison
| Feature | SheetJS (xlsx) | ExcelJS | node-xlsx |
|---|---|---|---|
| XLSX read/write | ✅ | ✅ | ✅ |
| XLS (legacy) | ✅ | ❌ | ❌ |
| CSV, ODS, Numbers | ✅ 20+ formats | ❌ | ❌ |
| Streaming read | ✅ (Pro) | ✅ | ❌ |
| Streaming write | ✅ (Pro) | ✅ | ❌ |
| Cell styling | ✅ Limited | ✅ Excellent | ❌ |
| Formulas | ✅ Read | ✅ Read + Write | ❌ |
| Charts | ❌ | ✅ | ❌ |
| Conditional formatting | ❌ | ✅ | ❌ |
| Pivot tables | ❌ | ✅ | ❌ |
| Browser support | ✅ | ✅ | ✅ |
| TypeScript | ✅ | ✅ | ✅ |
| Bundle size | ~900KB | ~600KB | ~1MB (wraps xlsx) |
When to Use Each
Choose SheetJS if:
- Reading legacy XLS or other non-XLSX formats (ODS, Numbers, CSV)
- You need browser + Node.js support with the same API
- Simple JSON ↔ spreadsheet conversion without formatting
Choose ExcelJS if:
- Generating formatted Excel reports (colors, fonts, borders, charts)
- Processing large files via streaming (memory efficiency)
- Pivot tables, conditional formatting, or advanced Excel features
Choose node-xlsx if:
- Maximum simplicity for basic XLSX read/write
- TypeScript project that needs tiny API surface
- You're processing simple 2D data without any cell formatting
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on SheetJS Community Edition 0.20.x, ExcelJS 4.x, and node-xlsx 0.23.x.