Back to Blog
CSVExcelData ExportNode.js

Data Export: CSV and Excel Generation

Export data for users and integrations. From CSV generation to Excel files to streaming large datasets.

B
Bootspring Team
Engineering
September 28, 2023
6 min read

Users need to export data for analysis, reporting, and integration with other tools. Here's how to generate CSV and Excel files efficiently.

CSV Generation#

1import { stringify } from 'csv-stringify'; 2import { Readable } from 'stream'; 3 4// Simple CSV generation 5function generateCsv( 6 data: Record<string, any>[], 7 columns: { key: string; header: string }[] 8): Promise<string> { 9 return new Promise((resolve, reject) => { 10 const stringifier = stringify({ 11 header: true, 12 columns: columns.map((c) => ({ key: c.key, header: c.header })), 13 }); 14 15 const chunks: string[] = []; 16 17 stringifier.on('readable', () => { 18 let chunk; 19 while ((chunk = stringifier.read())) { 20 chunks.push(chunk); 21 } 22 }); 23 24 stringifier.on('error', reject); 25 stringifier.on('finish', () => resolve(chunks.join(''))); 26 27 data.forEach((row) => stringifier.write(row)); 28 stringifier.end(); 29 }); 30} 31 32// Usage 33const csv = await generateCsv(orders, [ 34 { key: 'id', header: 'Order ID' }, 35 { key: 'customerName', header: 'Customer' }, 36 { key: 'total', header: 'Total' }, 37 { key: 'createdAt', header: 'Date' }, 38]); 39 40// API endpoint 41app.get('/export/orders', authenticate, async (req, res) => { 42 const orders = await prisma.order.findMany({ 43 where: { userId: req.user.id }, 44 include: { customer: true }, 45 }); 46 47 const data = orders.map((o) => ({ 48 id: o.id, 49 customerName: o.customer.name, 50 total: o.total, 51 createdAt: o.createdAt.toISOString(), 52 })); 53 54 const csv = await generateCsv(data, [ 55 { key: 'id', header: 'Order ID' }, 56 { key: 'customerName', header: 'Customer' }, 57 { key: 'total', header: 'Total ($)' }, 58 { key: 'createdAt', header: 'Date' }, 59 ]); 60 61 res.setHeader('Content-Type', 'text/csv'); 62 res.setHeader('Content-Disposition', 'attachment; filename="orders.csv"'); 63 res.send(csv); 64});

Streaming Large CSV#

1import { stringify } from 'csv-stringify'; 2import { pipeline } from 'stream/promises'; 3 4// Stream CSV for large datasets 5async function streamCsvExport( 6 req: Request, 7 res: Response, 8 query: () => AsyncIterable<any>, 9 columns: { key: string; header: string }[], 10 filename: string 11) { 12 res.setHeader('Content-Type', 'text/csv'); 13 res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); 14 15 const stringifier = stringify({ 16 header: true, 17 columns: columns.map((c) => ({ key: c.key, header: c.header })), 18 }); 19 20 // Stream data from database 21 const dataStream = Readable.from(query()); 22 23 await pipeline(dataStream, stringifier, res); 24} 25 26// Prisma cursor-based streaming 27async function* streamOrders(userId: string) { 28 let cursor: string | undefined; 29 const batchSize = 1000; 30 31 while (true) { 32 const orders = await prisma.order.findMany({ 33 where: { userId }, 34 take: batchSize, 35 skip: cursor ? 1 : 0, 36 cursor: cursor ? { id: cursor } : undefined, 37 orderBy: { id: 'asc' }, 38 }); 39 40 if (orders.length === 0) break; 41 42 for (const order of orders) { 43 yield { 44 id: order.id, 45 total: order.total, 46 status: order.status, 47 createdAt: order.createdAt.toISOString(), 48 }; 49 } 50 51 cursor = orders[orders.length - 1].id; 52 } 53} 54 55// Usage 56app.get('/export/orders/stream', authenticate, async (req, res) => { 57 await streamCsvExport( 58 req, 59 res, 60 () => streamOrders(req.user.id), 61 [ 62 { key: 'id', header: 'Order ID' }, 63 { key: 'total', header: 'Total' }, 64 { key: 'status', header: 'Status' }, 65 { key: 'createdAt', header: 'Date' }, 66 ], 67 'orders.csv' 68 ); 69});

Excel Generation#

1import ExcelJS from 'exceljs'; 2 3async function generateExcel( 4 data: Record<string, any>[], 5 columns: { key: string; header: string; width?: number }[], 6 sheetName = 'Sheet1' 7): Promise<Buffer> { 8 const workbook = new ExcelJS.Workbook(); 9 const worksheet = workbook.addWorksheet(sheetName); 10 11 // Set columns 12 worksheet.columns = columns.map((col) => ({ 13 header: col.header, 14 key: col.key, 15 width: col.width || 15, 16 })); 17 18 // Style header row 19 worksheet.getRow(1).font = { bold: true }; 20 worksheet.getRow(1).fill = { 21 type: 'pattern', 22 pattern: 'solid', 23 fgColor: { argb: 'FFE0E0E0' }, 24 }; 25 26 // Add data 27 worksheet.addRows(data); 28 29 // Auto-filter 30 worksheet.autoFilter = { 31 from: 'A1', 32 to: `${String.fromCharCode(64 + columns.length)}1`, 33 }; 34 35 return workbook.xlsx.writeBuffer() as Promise<Buffer>; 36} 37 38// Multi-sheet workbook 39async function generateReportWorkbook(report: Report): Promise<Buffer> { 40 const workbook = new ExcelJS.Workbook(); 41 workbook.creator = 'Your App'; 42 workbook.created = new Date(); 43 44 // Summary sheet 45 const summarySheet = workbook.addWorksheet('Summary'); 46 summarySheet.addRow(['Report Period', report.period]); 47 summarySheet.addRow(['Generated', new Date().toISOString()]); 48 summarySheet.addRow([]); 49 summarySheet.addRow(['Total Orders', report.totalOrders]); 50 summarySheet.addRow(['Total Revenue', report.totalRevenue]); 51 52 // Orders sheet 53 const ordersSheet = workbook.addWorksheet('Orders'); 54 ordersSheet.columns = [ 55 { header: 'Order ID', key: 'id', width: 20 }, 56 { header: 'Customer', key: 'customer', width: 25 }, 57 { header: 'Amount', key: 'amount', width: 15 }, 58 { header: 'Date', key: 'date', width: 20 }, 59 ]; 60 ordersSheet.addRows(report.orders); 61 62 // Format currency column 63 ordersSheet.getColumn('amount').numFmt = '$#,##0.00'; 64 65 // Products sheet 66 const productsSheet = workbook.addWorksheet('Products'); 67 productsSheet.columns = [ 68 { header: 'Product', key: 'name', width: 30 }, 69 { header: 'Units Sold', key: 'units', width: 15 }, 70 { header: 'Revenue', key: 'revenue', width: 15 }, 71 ]; 72 productsSheet.addRows(report.products); 73 74 return workbook.xlsx.writeBuffer() as Promise<Buffer>; 75} 76 77// API endpoint 78app.get('/export/report', authenticate, async (req, res) => { 79 const report = await generateReport(req.user.id); 80 const buffer = await generateReportWorkbook(report); 81 82 res.setHeader( 83 'Content-Type', 84 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 85 ); 86 res.setHeader('Content-Disposition', 'attachment; filename="report.xlsx"'); 87 res.send(buffer); 88});

Streaming Excel#

1// Stream large Excel files 2async function streamExcelExport( 3 res: Response, 4 query: () => AsyncIterable<any>, 5 columns: { key: string; header: string }[], 6 filename: string 7) { 8 const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({ 9 stream: res, 10 useStyles: true, 11 }); 12 13 res.setHeader( 14 'Content-Type', 15 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 16 ); 17 res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); 18 19 const worksheet = workbook.addWorksheet('Data'); 20 21 // Set columns 22 worksheet.columns = columns.map((col) => ({ 23 header: col.header, 24 key: col.key, 25 width: 20, 26 })); 27 28 // Stream rows 29 for await (const row of query()) { 30 worksheet.addRow(row).commit(); 31 } 32 33 await worksheet.commit(); 34 await workbook.commit(); 35}

Background Export Jobs#

1// Queue large exports 2async function queueExport( 3 userId: string, 4 type: string, 5 filters: Record<string, any> 6): Promise<string> { 7 const exportJob = await prisma.exportJob.create({ 8 data: { 9 userId, 10 type, 11 filters, 12 status: 'pending', 13 }, 14 }); 15 16 await exportQueue.add('generate-export', { 17 jobId: exportJob.id, 18 }); 19 20 return exportJob.id; 21} 22 23// Worker 24const exportWorker = new Worker( 25 'exports', 26 async (job) => { 27 const { jobId } = job.data; 28 29 const exportJob = await prisma.exportJob.findUnique({ 30 where: { id: jobId }, 31 }); 32 33 try { 34 await prisma.exportJob.update({ 35 where: { id: jobId }, 36 data: { status: 'processing' }, 37 }); 38 39 // Generate export 40 const buffer = await generateExport(exportJob.type, exportJob.filters); 41 42 // Upload to S3 43 const key = `exports/${exportJob.userId}/${jobId}.xlsx`; 44 await s3.putObject({ 45 Bucket: process.env.S3_BUCKET, 46 Key: key, 47 Body: buffer, 48 }); 49 50 // Update job 51 await prisma.exportJob.update({ 52 where: { id: jobId }, 53 data: { 54 status: 'completed', 55 fileKey: key, 56 completedAt: new Date(), 57 }, 58 }); 59 60 // Notify user 61 await sendExportReadyEmail(exportJob.userId, jobId); 62 } catch (error) { 63 await prisma.exportJob.update({ 64 where: { id: jobId }, 65 data: { 66 status: 'failed', 67 error: error.message, 68 }, 69 }); 70 } 71 }, 72 { connection: redis } 73); 74 75// Check status 76app.get('/exports/:id', authenticate, async (req, res) => { 77 const exportJob = await prisma.exportJob.findFirst({ 78 where: { 79 id: req.params.id, 80 userId: req.user.id, 81 }, 82 }); 83 84 if (!exportJob) { 85 return res.status(404).json({ error: 'Not found' }); 86 } 87 88 if (exportJob.status === 'completed') { 89 const downloadUrl = await getSignedUrl(exportJob.fileKey); 90 return res.json({ status: exportJob.status, downloadUrl }); 91 } 92 93 res.json({ status: exportJob.status }); 94});

Best Practices#

Performance: ✓ Stream large datasets ✓ Use cursor pagination ✓ Queue heavy exports ✓ Set memory limits User Experience: ✓ Show progress for large exports ✓ Email when complete ✓ Provide format options ✓ Include metadata (date, filters) Data: ✓ Format dates consistently ✓ Handle null values ✓ Escape special characters ✓ Include headers

Conclusion#

Data export is essential for user productivity. Stream large datasets, queue heavy exports, and provide format options. Always test exports with real data volumes to ensure performance.

Share this article

Help spread the word about Bootspring