I have a route.ts file under api folder like this:
import { NextRequest, NextResponse } from "next/server";
import { conditionalEvergreenDownloadQuery } from "@/app/lib/data";
import { Readable } from "stream";
import { createReadStream } from "fs";
type ReportHeaders = Array<
keyof Awaited<ReturnType<typeof conditionalEvergreenDownloadQuery>>[number]
>;
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
// Check if search params are defined
const hasSearchParams = Array.from(searchParams.entries()).length > 0;
try {
let dataStream: Readable;
if (hasSearchParams) {
// Use existing query params logic
const queryParams = {
it_service: searchParams.get("service") || undefined,
it_org_6: searchParams.get("org_6") || undefined,
it_org_7: searchParams.get("org_7") || undefined,
it_org_8: searchParams.get("org_8") || undefined,
software_product_version_name:
searchParams.get("software_product_version_name") || undefined,
service_owner: searchParams.get("service_owner") || undefined,
eg_status: searchParams.get("eg_status") || undefined,
eim_scope:
searchParams.get("eim_scope") === null
? undefined
: searchParams.get("eim_scope") === "true",
};
const data = await conditionalEvergreenDownloadQuery(queryParams);
// Get headers that are inferred from the underlying query
const headers = Object.keys(data[0]) as ReportHeaders;
// Create a readable stream from the data
dataStream = Readable.from(generateCSV(data, headers));
} else {
// If no search params, read from disk
dataStream = createReadStream(process.env.FULL_REPORT || "");
}
// Set up the response
const response = new NextResponse(dataStream as any);
response.headers.set("Content-Type", "text/csv");
response.headers.set(
"Content-Disposition",
`attachment; filename=evergreen_inventory_${new Date().toISOString()}.csv`
);
return response;
} catch (error) {
return NextResponse.json(
{ error: `Failed to generate csv with error: ${error}` },
{ status: 500 }
);
}
}
async function* generateCSV(
data: Awaited<ReturnType<typeof conditionalEvergreenDownloadQuery>>,
headers: Array<
keyof Awaited<ReturnType<typeof conditionalEvergreenDownloadQuery>>[number]
>
) {
// Yield the header row
yield headers.join(",") + "n";
// Yield each data row
for (const record of data) {
const row = headers.map((header) => {
const value = record[header];
return escapeCsvValue(value);
});
yield row.join(",") + "n";
}
}
function escapeCsvValue(value: any): string {
if (value === null || value === undefined) {
return "";
}
const stringValue = String(value);
if (
stringValue.includes('"') ||
stringValue.includes(",") ||
stringValue.includes("n")
) {
return `"${stringValue.replace(/"/g, '""')}"`;
}
return stringValue;
}
and the download button.tsx is ca clinet component that calls that api route:
"use client";
import { toast } from "@/components/ui/use-toast";
import { Download } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
export function DownloadCSVButton() {
const searchParams = useSearchParams();
const [isDownloading, setIsDownloading] = useState(false);
const handleDownload = async () => {
setIsDownloading(true);
const params = new URLSearchParams(searchParams);
params.delete("page");
const downloadUrl = `/dashboard/inventory/api/?${params.toString()}`;
try {
const response = await fetch(downloadUrl);
if (!response.ok) throw new Error("Download Failed");
const reader = response.body?.getReader();
const filename =
response.headers.get("Content-Disposition")?.split("filename=")[1] ||
"evergreen_inventory.csv";
if (!reader) throw new Error("Unable to read response");
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const blob = new Blob(chunks, { type: "text/csv" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
window.URL.revokeObjectURL(url);
toast({
title: "Success",
description: "Report has been generated",
duration: 5000,
});
} catch (error) {
toast({
title: `Error: ${error}`,
description: "Failed to download CSV file, Please try again",
variant: "destructive",
duration: 5000,
});
} finally {
setIsDownloading(false);
}
};
return (
<button
className="flex h-9 items-center rounded-lg bg-blue-600 from-blue-300 to-blue-950 px-4 text-sm font-medium text-white transition-colors hover:bg-gradient-to-r focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
disabled={isDownloading}
aria-label={isDownloading ? "Downloading ...." : "Export"}
onClick={handleDownload}
>
<span>{isDownloading ? "Downloading ...." : "Export"}</span>
<Download className="ml-2 h-4 w-4"></Download>
</button>
);
}
I noticed the downloads if I access the server directly, that is not behind the reverse proxy of nginx are considerably faster than behind the proxy. Why is that ? how can i improve this ?