Import & Export Excel (XLSX) Files in JavaScript

Reading and writing Excel files in JavaScript is easy for raw values and surprisingly hard for everything else — styles, formulas, merged cells, and images. This guide covers importing and exporting XLSX, CSV, and JSON in the browser and on the server, the round-trip fidelity problem that bites most teams, and how to decide between a file parser and a full spreadsheet engine.
Almost every data-heavy app eventually needs to read an uploaded spreadsheet or hand the user a downloadable .xlsx. JavaScript can do both — in the browser and on Node — but the gap between 'parse the numbers' and 'preserve the workbook' is where most projects underestimate the work.
This guide walks through importing and exporting XLSX, CSV, and JSON, the round-trip fidelity problem, the browser-versus-server tradeoff, and when a file parser is enough versus when you actually need a spreadsheet engine.
How do you read an XLSX file in JavaScript?
An .xlsx file is a zip of XML parts, so you never want to parse it by hand. In the browser you read the uploaded file as an ArrayBuffer; on Node you read it from disk as a Buffer. The parsing call is the same either way:
// Browser: from a file <input>
async function readWorkbook(file: File) {
const buffer = await file.arrayBuffer();
const workbook = parseXlsx(buffer); // your parser of choice
const sheet = workbook.sheets[0];
// Iterate rows -> array of objects keyed by header
const [headers, ...rows] = sheet.toArray();
return rows.map((row) =>
Object.fromEntries(row.map((cell, i) => [headers[i], cell]))
);
}
// Node: from disk
import { readFile } from "node:fs/promises";
const buffer = await readFile("./report.xlsx");
const workbook = parseXlsx(buffer);That handles values. What it quietly drops is everything that makes the file a spreadsheet: number formats, cell styles, formulas (you usually get the cached result, not the formula), merged ranges, frozen panes, and embedded images.
How do you export an XLSX file for download?
Exporting is the reverse: build a workbook structure in memory, serialize it to a binary blob, and trigger a download in the browser (or write it to disk on the server).
function exportToXlsx(rows: Record<string, unknown>[]) {
const headers = Object.keys(rows[0]);
const matrix = [headers, ...rows.map((r) => headers.map((h) => r[h]))];
const blob = buildXlsx(matrix); // -> Blob (application/vnd...sheet)
// Trigger a browser download
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "export.xlsx";
a.click();
URL.revokeObjectURL(url);
}This produces a valid file Excel will open. But it is a flat dump of values — no column widths, no currency or date formatting, no styling. If your users expect the export to look like what they saw on screen, a plain value dump will disappoint them.
CSV and JSON
- CSV is text, so it carries zero formatting, formulas, or types — every value is a string and you parse types yourself. Watch for quoting, embedded commas, newlines in cells, and the BOM Excel expects for UTF-8.
- JSON is the natural shape for app state and APIs, but it has no native concept of a sheet, a formula, or a style — you define your own schema and lose anything you don't model.
- Both are great for data interchange and terrible for fidelity. Reach for them when you only care about values, not appearance.
What is round-trip fidelity, and why does it break?
The real difficulty is not reading or writing — it is preserving the workbook when a file goes in and comes back out. A user uploads a styled, formula-driven .xlsx, edits a few cells, and downloads it again. With a naive read-then-write pipeline, the file that comes back has lost its formulas (replaced by stale values), its number formats, its merged cells, and its images.
Preserving fidelity means modeling the whole workbook — styles, formats, formulas, defined names, merges, images — not just a 2D array of values. That is a materially bigger job than a CSV parser, and it is the line that separates a file toolkit from a spreadsheet engine.
Should you parse Excel in the browser or on the server?
| Browser | Server (Node) | |
|---|---|---|
| File never leaves device | Yes — good for privacy | No — upload required |
| Large files | Limited by tab memory | Scales with the box |
| Generated downloads | Instant, no round-trip | Stream the response |
| CPU-heavy parsing | Can freeze the UI | Offload to a worker/queue |
| Best for | Editing and instant export | Batch jobs, big data, automation |
In the browser, keep heavy parsing off the main thread with a Web Worker so the UI stays responsive. On the server, treat very large imports as background jobs rather than blocking a request. Many apps do both: parse small files client-side for instant feedback, and route large or trusted batch imports through the server.
Parser vs. engine: which do you need?
There are two distinct tools here, and picking the wrong one wastes weeks.
A file toolkit like SheetJS reads and writes a huge range of formats — 20+ including XLSX, CSV, ODS, and more. The Community build is free under Apache-2.0, with a paid Pro tier adding extras; it reads and writes formula text but does not recalculate formulas for you. Crucially, it has no UI: it converts files to and from data, and that is the whole job. If all you need is to ingest an upload or generate a download, that is exactly the right scope.
A spreadsheet engine does that and adds an interactive grid: the user sees the sheet, edits cells, formulas recalculate, and the export reflects what they edited. WorksheetJS does full-fidelity XLSX, CSV, and JSON import-export entirely in the browser — styles, formulas, and images preserved on round-trip — and renders an editable spreadsheet on top. Install it from npm as @worksheet-js/core, with framework bindings like @worksheet-js/react.
import { Worksheet } from "@worksheet-js/react";
// `ws` is the mounted <Worksheet /> instance (accessed via ref)
// Import: parse an uploaded file (.xlsx, .csv, .html) — styles + formulas intact
await ws.importFromFile(file);
// ...user views and edits the live spreadsheet...
// Export: round-trip back to .xlsx with fidelity preserved (triggers a download)
await ws.exportXlsx("edited.xlsx");
// Or get a buffer to send to your server
const buffer = await ws.exportToBuffer();
await fetch("/api/save", { method: "POST", body: buffer });| Approach | Good for |
|---|---|
| CSV/JSON parse | Pure data interchange where appearance does not matter |
| XLSX parser (e.g. SheetJS) | Importing or generating files server-side or client-side, no UI |
| Spreadsheet engine (WorksheetJS) | Displaying and editing the data, with full-fidelity round-trip |
What are the common Excel import/export pitfalls?
- Reading the formula's cached value and assuming it will recalculate later — it will not unless something evaluates it.
- Forgetting the UTF-8 BOM on CSV, so Excel mangles accented characters.
- Treating dates as numbers (Excel stores them as serial numbers) and exporting raw serials.
- Parsing large files on the main thread and freezing the tab.
- Round-tripping through a value-only model and silently stripping styles, merges, and images.
Conclusion
Importing and exporting Excel in JavaScript is straightforward for raw values and genuinely hard for fidelity. Scope the job honestly: if you only move data, a parser is enough; if users will see and edit the sheet and expect it back intact, reach for a spreadsheet engine that handles the full-fidelity round-trip for you.


