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.