To convert deeply nested JSON into CSV in a Next.js App Router app, define how objects and arrays map to columns, then run parsing, flattening, and CSV serialization in a dedicated Web Worker. Keep file selection, progress, errors, and downloads in a small Client Component. A worker keeps that computation off the page’s main thread, but it does not guarantee a speedup or prove that the whole application never sends data over the network.
Choose the CSV mapping before writing code
CSV represents rows and fields; it does not define how nested JSON structures should be laid out. The converter below uses a deliberately explicit policy:
As an Amazon Associate I earn from qualifying purchases.
- Each top-level array item becomes one CSV record. A single top-level object becomes one record.
- Nested objects become columns named with dot-separated paths, such as
customer.name. - Arrays within a record are kept in one cell as JSON text. They do not create additional rows or indexed columns.
- Explicit
nullbecomes the textnull. Missing properties produce an empty cell. - Empty objects and arrays do not create columns. If there are no scalar values at all, the converter reports that it has no columns to export.
- Flattened paths are escaped so literal dots and backslashes in object keys cannot collide with path separators. Duplicate headers are also rejected rather than silently overwritten.
For example, this input:
{
"customer": { "name": "Ava", "active": true },
"order": { "id": 17 },
"note": "first, then "quoted""
}
produces one record with headers customer.name, customer.active, order.id, and note. Its last cell is quoted and the embedded quotation marks are doubled. For an array of records, the converter takes the union of headers across all records and leaves a field empty wherever a record lacks that property.
Free tools Windows power users keep installed
One-click scans. No signup required.
Keep the browser UI in a Client Component
Next.js App Router pages and layouts are Server Components by default. Browser APIs and interactive behavior belong in a Client Component marked with use client; keep that boundary narrow and render static page content on the server where practical. See Next.js Server and Client Components and the use client directive.
#1 Best Overall
The page component can render a client-side converter. The file input, worker lifecycle, status messages, and download link all live in that client component:
// app/json-csv/page.tsx
import JsonCsvConverter from './JsonCsvConverter';
export default function Page() {
return (
<main>
<h1>JSON to CSV</h1>
<p>Select a JSON file to convert it in your browser.</p>
<JsonCsvConverter />
</main>
);
}
Put parsing, flattening, and serialization in a worker
A Web Worker runs in a separate context and exchanges messages with its owning page; it cannot update the page DOM directly. The main thread should own UI state while the worker returns a result or error. See MDN’s Web Workers guide.
Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
This example uses a module worker created from a bundler-recognized URL. Confirm this worker entry-point pattern with the bundler and Next.js version used by your project, since the cited framework guidance does not establish one packaging setup for every toolchain.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteDefine and serialize the flattened records
Save the worker logic in app/json-csv/csv.worker.ts. It gathers headers in first-seen order, uses own properties to distinguish a missing field from a present value, and serializes each cell—including headers—using CSV escaping rules.
Rank #3
type Cell = string;
type FlatRow = Record<string, Cell>;
function escapeSegment(segment: string): string {
return segment.replace(/\/g, '\\').replace(/./g, '\.');
}
function csvField(value: string): string {
if (/[",rn]/.test(value)) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}
function cellText(value: unknown): string {
if (value === null) return 'null';
if (Array.isArray(value)) return JSON.stringify(value);
if (typeof value === 'object') return '';
return String(value);
}
function flatten(value: unknown, prefix = '', row: FlatRow = {}): FlatRow {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
if (prefix) row[prefix] = cellText(value);
return row;
}
for (const [key, child] of Object.entries(value)) {
const path = prefix ? `${prefix}.${escapeSegment(key)}` : escapeSegment(key);
if (child !== null && typeof child === 'object' && !Array.isArray(child)) {
flatten(child, path, row);
} else {
row[path] = cellText(child);
}
}
return row;
}
self.onmessage = (event: MessageEvent<{ text: string }>) => {
try {
const parsed: unknown = JSON.parse(event.data.text);
const inputs = Array.isArray(parsed) ? parsed : [parsed];
if (!inputs.every(item => item !== null && typeof item === 'object' && !Array.isArray(item))) {
throw new Error('The JSON root must be an object or an array of objects.');
}
const rows = inputs.map(item => flatten(item));
const headers = [...new Set(rows.flatMap(row => Object.keys(row)))];
if (headers.length === 0) throw new Error('No scalar fields were found to export.');
const csv = [
headers.map(csvField).join(','),
...rows.map(row =>
headers.map(header => csvField(Object.hasOwn(row, header) ? row[header] : '')).join(',')
)
].join('rn');
self.postMessage({ ok: true, csv });
} catch (error) {
self.postMessage({
ok: false,
error: error instanceof Error ? error.message : 'Conversion failed.'
});
}
};
The example treats a root array as a list of records, not as an array-valued field. Nested arrays inside a record stay intact as JSON text; that choice avoids multiplying rows or inventing column names, but consumers that require one scalar per cell may prefer a different policy. Because values with no path are skipped, a top-level primitive is rejected by the root-shape check rather than exported as a row.
Connect the worker to file selection and download
In app/json-csv/JsonCsvConverter.tsx, read the selected file as text and send it to the worker. The file’s text is passed rather than a parsed object, so parsing also happens off the main thread. The browser receives the resulting CSV and creates a downloadable Blob URL; revoke the previous URL when a new conversion starts and when the component unmounts.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
'use client';
import { useEffect, useRef, useState } from 'react';
type WorkerReply = { ok: true; csv: string } | { ok: false; error: string };
export default function JsonCsvConverter() {
const workerRef = useRef<Worker | null>(null);
const urlRef = useRef<string | null>(null);
const [status, setStatus] = useState('Choose a JSON file.');
const [downloadUrl, setDownloadUrl] = useState('');
useEffect(() => {
const worker = new Worker(new URL('./csv.worker.ts', import.meta.url), { type: 'module' });
workerRef.current = worker;
worker.onmessage = (event: MessageEvent<WorkerReply>) => {
if (!event.data.ok) {
setStatus(event.data.error);
return;
}
const url = URL.createObjectURL(new Blob([event.data.csv], { type: 'text/csv;charset=utf-8' }));
urlRef.current = url;
setDownloadUrl(url);
setStatus('Conversion complete.');
};
worker.onerror = () => setStatus('The conversion worker failed. Try again with a valid JSON file.');
return () => {
worker.terminate();
if (urlRef.current) URL.revokeObjectURL(urlRef.current);
};
}, []);
async function handleFile(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
if (!file || !workerRef.current) return;
if (urlRef.current) URL.revokeObjectURL(urlRef.current);
urlRef.current = null;
setDownloadUrl('');
setStatus('Reading and converting…');
try {
workerRef.current.postMessage({ text: await file.text() });
} catch {
setStatus('Could not read that file. Select it again or choose another JSON file.');
}
}
return (
<section>
<label>
JSON file
<input type="file" accept="application/json,.json" onChange={handleFile} />
</label>
<p role="status">{status}</p>
{downloadUrl && (
<a href={downloadUrl} download="converted.csv">Download CSV</a>
)}
</section>
);
}
In a production component, account for multiple selections while a conversion is already running—for example, attach a request ID to each message and ignore replies that do not match the latest request. Add a progress protocol only if the worker can report meaningful stages; a single synchronous conversion cannot send intermediate progress while it is busy without being structured to yield or divide work.
Why a worker may not make every conversion faster
Worker messages commonly use structured cloning to copy data between contexts. Sending a large parsed object to a worker and then copying a large result back can consume time and memory of its own. Sending the source text once, parsing and converting there, and returning the final CSV avoids transferring the parsed object in both directions, but still involves moving the text and output. Measure your actual inputs and target devices; there is no source-backed universal size threshold or speedup figure.
Best Value
A worker running locally does not establish that the entire application is private or offline. That claim depends on the surrounding code and network behavior as well as the worker. Inspect the application’s data path before promising that user files never leave the browser.
CSV escaping and compatibility
RFC 4180 describes common CSV conventions. It says: “Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in double-quotes.” Embedded double quotes are doubled. The serializer applies this rule to both headers and values, and uses CRLF between records; every data row follows the same header order and has one cell per header. See RFC 4180.
The RFC is informational, and CSV consumers differ. If a downstream tool requires a particular encoding, delimiter, line-ending convention, or spreadsheet-specific behavior, make that compatibility target explicit and test a representative export with that tool.
Do not use Next.js’s worker script strategy for this job
Next.js documents <Script strategy="worker"> as experimental, requiring the nextScriptWorkers flag and available only in the Pages Router, not the App Router. That feature uses Partytown to offload third-party scripts; it is not a general-purpose worker setup for parsing and converting application data. See the Next.js Scripts guide. Framework documentation can change, so check the guide for the version you deploy.
Quick Recap
Validate the edge cases that define your output
- Test commas, quotation marks, CRLF and LF newlines in keys and values; verify quotes are doubled and fields remain in the intended columns.
- Test records with different nested keys; confirm missing properties become empty cells and explicit nulls become
null. - Test keys that contain dots or backslashes, plus empty objects and arrays, to ensure headers follow the documented naming and omission rules.
- Test empty input arrays, malformed JSON, and arrays containing non-object items; surface a clear error instead of downloading a misleading file.
- Try representative files on the devices and browsers your application supports. Track memory and responsiveness as well as elapsed time, because the worker and cloned messages have costs too.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




