Here is a real-world example, much more advanced, with Sentry for debugging, and returning a stream with dynamic CSV filename. (and TypeScript types)
It's probably not as helpful as the other answer (due to its complexity) but it might be interesting to have a more complete real-world example.
Note that I'm not familiar with streams, I'm not 100% what I'm doing is the most efficient way to go, but it does work.
src/pages/api/webhooks/downloadCSV.ts
import { logEvent } from '@/modules/core/amplitude/amplitudeServerClient';
import {
AMPLITUDE_API_ENDPOINTS,
AMPLITUDE_EVENTS,
} from '@/modules/core/amplitude/events';
import { createLogger } from '@/modules/core/logging/logger';
import { ALERT_TYPES } from '@/modules/core/sentry/config';
import { configureReq } from '@/modules/core/sentry/server';
import { flushSafe } from '@/modules/core/sentry/universal';
import * as Sentry from '@sentry/node';
import {
NextApiRequest,
NextApiResponse,
} from 'next';
import stream, { Readable } from 'stream';
import { promisify } from 'util';
const fileLabel = 'api/webhooks/downloadCSV';
const logger = createLogger({
fileLabel,
});
const pipeline = promisify(stream.pipeline);
type EndpointRequestQuery = {
/**
* Comma-separated CSV string.
*
* Will be converted into an in-memory stream and sent back to the browser so it can be downloaded as an actual CSV file.
*/
csvAsString: string;
/**
* Name of the file to be downloaded.
*
* @example john-doe.csv
*/
downloadAs: string;
};
type EndpointRequest = NextApiRequest & {
query: EndpointRequestQuery;
};
/**
* Reads a CSV string and returns it as a CSV file that can be downloaded.
*
* @param req
* @param res
*
* @method GET
*
* @example https://753f-80-215-115-17.ngrok.io/api/webhooks/downloadCSV?downloadAs=bulk-orders-for-student-ambroise-dhenain-27.csv&csvAsString=beneficiary_name%2Ciban%2Camount%2Ccurrency%2Creference%0AAmbroise%20Dhenain%2CFR76%204061%208802%208600%200404%208805%20373%2C400%2CEUR%2CBooster%20Unly%20%20septembre%0AAmbroise%20Dhenain%2CFR76%204061%208802%208600%200404%208805%20373%2C400%2CEUR%2CBooster%20Unly%20%20octobre%0AAmbroise%20Dhenain%2CFR76%204061%208802%208600%200404%208805%20373%2C400%2CEUR%2CBooster%20Unly%20%20novembre%0A
*/
export const downloadCSV = async (req: EndpointRequest, res: NextApiResponse): Promise<void> => {
try {
configureReq(req, { fileLabel });
const {
csvAsString,
downloadAs = 'data.csv',
} = req?.query as EndpointRequestQuery;
await logEvent(AMPLITUDE_EVENTS.API_INVOKED, null, {
apiEndpoint: AMPLITUDE_API_ENDPOINTS.WEBHOOK_DOWNLOAD_CSV,
});
Sentry.withScope((scope): void => {
scope.setTag('alertType', ALERT_TYPES.WEBHOOK_DOWNLOAD_CSV);
Sentry.captureEvent({
message: `[downloadCSV] Received webhook callback.`,
level: Sentry.Severity.Log,
});
});
await flushSafe();
res.setHeader('Content-Type', 'application/csv');
res.setHeader('Content-Disposition', `attachment; filename=${downloadAs}`);
res.status(200);
await pipeline(Readable.from(new Buffer(csvAsString)), res);
} catch (e) {
Sentry.captureException(e);
logger.error(e.message);
await flushSafe();
res.status(500);
res.end();
}
};
export default downloadCSV;
Code is based on the Next Right Now boilerplate, if you want to dive-in into the configuration (Sentry, etc.): https://github.com/UnlyEd/next-right-now/blob/v2-mst-aptd-at-lcz-sty/src/pages/api/webhooks/deploymentCompleted.ts