TabularJS vs ExcelJS
ExcelJS is a full-featured Excel toolkit — read, write, style, stream — with a workbook-centric object model. If all you need is the data out of a file, that model becomes boilerplate: open the workbook, pick the sheet, iterate rows, trim off the 1-indexed padding. TabularJS collapses that into one async call, with the same output shape across 16+ formats.
At a glance
| Feature | TabularJS | ExcelJS |
|---|---|---|
| Dependencies | Zero | Several runtime dependencies |
| License | MIT | MIT |
| Formats read | 16+ (XLSX, XLS, ODS, CSV, HTML, DBF, SYLK, DIF, Lotus) | XLSX, CSV (primarily) |
| API surface | Single await tabularjs(file) call | Workbook → worksheets → rows → cells |
| Output shape | Array-of-arrays, drop-in for Jspreadsheet | Row objects (1-indexed) |
| Write / mutate workbooks | Read-only | Yes — rich styling & streaming writers |
| Streaming reader | No | Yes |
Stick with ExcelJS if you are generating Excel files, styling them on the server, or streaming rows through gigabyte-scale workbooks. Switch to TabularJS when the goal is simply to read a file into JSON.
Migration: Node.js
A typical ExcelJS read pulls data out row by row, mindful of the 1-indexed row.values:
import ExcelJS from 'exceljs';
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile('./sales.xlsx');
const worksheet = workbook.getWorksheet(1);
const rows = [];
worksheet.eachRow({ includeEmpty: true }, (row) => {
// row.values is 1-indexed; drop the first slot
rows.push(row.values.slice(1));
});
console.log(rows);The TabularJS version is a single call:
import tabularjs from 'tabularjs';
const result = await tabularjs('./sales.xlsx');
console.log(result.worksheets[0].data);Migration: every worksheet
// ExcelJS — iterate every worksheet
await workbook.xlsx.readFile('./book.xlsx');
const sheets = workbook.worksheets.map((ws) => {
const data = [];
ws.eachRow({ includeEmpty: true }, (row) => data.push(row.values.slice(1)));
return { name: ws.name, data };
});// TabularJS — same thing, already shaped
const { worksheets } = await tabularjs('./book.xlsx');
// worksheets: [{ name, data, mergeCells, styles }, ...]TabularJS already returns an array of worksheets in the order they appear, each with name, data, mergeCells and styles.
Migration: browser upload
// ExcelJS browser upload
input.addEventListener('change', async (e) => {
const buffer = await e.target.files[0].arrayBuffer();
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer);
const ws = workbook.worksheets[0];
const rows = [];
ws.eachRow({ includeEmpty: true }, (row) => rows.push(row.values.slice(1)));
render(rows);
});// TabularJS browser upload
input.addEventListener('change', async (e) => {
const result = await tabularjs(e.target.files[0]);
render(result.worksheets[0].data);
});No ArrayBuffer conversion. No row padding to trim. And the same code now handles CSV, ODS, HTML tables and older formats without any branches.
When to stay with ExcelJS
- You need to write XLSX files with formatting, images or charts.
- You need a streaming reader for very large workbooks.
- You rely on cell-level styling APIs for export.
In many codebases the practical choice is to use TabularJS on the read path (uploads, imports, ETL) and keep ExcelJS for the write path — they coexist cleanly.
Start migrating
Replace your ExcelJS read calls with a single await tabularjs(file).

