5

I am new to the exporting CSV from JSON... I am pretty close to gettin this... but I dont know how to send the CSV back to the client from my express server.

The user selects the customers to export, and hits the export button... I'd like the CSV file to be sent back.

I got the JSON data converted to CSV on the server, how would I send this file back to the client?

Here is my express route so far -

    router.post('/SaveCSV', (req, res) => {

    const csv = JSONToCSV(req.body, { fields: ["Customer Name", "Business Name", "Customer Email", "Customer ID", "School ID", "Student Count", "Admin Count", "Courses Count" ]})

    
    
})

I have tried res.send, res.sendFile, and res.attachment.. any guidance?

Update***

    router.post('/SaveCSV', (req, res) => {

    const csv = JSONToCSV(req.body, { fields: ["Customer Name", "Business Name", "Customer Email", "Customer ID", "School ID", "Student Count", "Admin Count", "Courses Count" ]})

    res.attachment('CustomerData.csv').send(csv)
    
})

Here is my fetch call on the client --

const saveCsv = async (customers) => {
    const token = await getAccessTokenSilently();
    try {
        const response = await fetch('/api/SaveCSV', {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${token}`,
                "content-type": 'application/json; Charset=UTF-8'
            },
            body: JSON.stringify(customers)
        })
        const responseData = await response.json()
        console.log(responseData)
    } catch (error) {
        console.log(error)
    }
}
Nick McLean
  • 601
  • 1
  • 9
  • 23
  • You are right - res.json(csv) sends back the data just fine, but in a string. res.send(csv) and the attachment gives me "SyntaxError: Unexpected token , in JSON at position 15" – Nick McLean Jul 31 '20 at 21:19
  • I added the fetch call from the client as well as my route from the server above! Thanks for your help so far. Maybe its because I am awaiting response.json()? – Nick McLean Jul 31 '20 at 21:24
  • Dang - I have never requested anything other than json back! hmm. Would you be able to point me in the right direction? Would maybe be awaiting res.attachment? – Nick McLean Jul 31 '20 at 21:29
  • I'd love the user to download the file. Maybe arraybuffer? I have seen that before when dealing with uploading files – Nick McLean Jul 31 '20 at 21:31
  • Ahh so convert the CSV client side.... Idk why I went this way now either.. I guess I was just going down the hole of what I read how to do. I'll fix this! – Nick McLean Jul 31 '20 at 21:56

2 Answers2

9

On the server side, you can just send the CSV data back. By default that will come with content type text/html, though - better to send it with the correct type text/csv. This could be done by using res.type('text/csv') first.

Now, it is debatable if this is necessary if you don't navigate the browser to your endpoint anyway but consume the download programmatically in your frontend code, but: To mark it as a "download" (that should not be displayed but actually saved to a file), you can use res.attachment(). If you specify a filename, it will then also send that filename in the header but also set the content type automatically based on the extension. So you can use res.attachment('customers.csv').send(csv) for example. If a browser were to navigate to your URL, it would see the CSV as a file download with name customers.csv.

router.post('/SaveCSV', (req, res) => {
    const csv = JSONToCSV(req.body, { fields: ["Customer Name", "Business Name", "Customer Email", "Customer ID", "School ID", "Student Count", "Admin Count", "Courses Count" ]})
    
    res.attachment('customers.csv').send(csv)
})

On the client side, you could then get the raw response body as a blob using response.blob(), and you could have the user download that blob. For more explanation how that could be done, you can research online (there are lots of resources about that). A full example that gets the filename right automatically and works with legacy browsers as well would be here (which was also linked in a comment to this Stack Overflow answer) - note that you'll need text/csv instead of application/pdf or read the content type from the response.

Let me instead post a simplified example with less functionality:

const blob = await response.blob()
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = 'customers.csv'
link.click()
setTimeout(() => URL.revokeObjectURL(link.href), 0)

But let's think this over. According to the code you showed, all your server code does is take the data from the frontend, stuff it into a CSV and send it back. I would say, you don't need the server for that at all! You could do the whole thing in the frontend code to begin with!

const saveCsv = (customers) => {
  const csv = JSONToCSV(customers, { fields: ["Customer Name", "Business Name", "Customer Email", "Customer ID", "School ID", "Student Count", "Admin Count", "Courses Count" ]})
  
  const link = document.createElement('a')
  link.href = 'data:text/csv,' + encodeURIComponent(csv)
  link.download = 'customers.csv'
  link.click()
}

This assumes of course that you can make the JSONToCSV library available in the frontend, or that you would add your own function to build the CSV data.

CherryDT
  • 25,571
  • 5
  • 49
  • 74
  • Wow~ Yup! Thats all I can say.. I am glad you were able to help with correcting my lack of understanding! What a great time to learn about res.blob and sending files over a server! -- I am happy you took the time to guide me! Thank you! – Nick McLean Aug 01 '20 at 12:13
2

The JSONtoCSV probably generates a string in the format of CSV from JSON.

From this string, you have two options.

  1. send the string into a text document, then res.sendFile that file.

    This can be done as such.

    I recommend using this package, although you can do it vanilla. Just create the file and write the string to it, then send it via res.sendFile.

    const csv = new ObjectsToCsv(csvdata);
    
    await csv.toDisk("./data.csv");
    
    res.sendFile("./data.csv");
    
  2. send the string as JSON, then parse it into a download in the frontend. This is the most common approach.

    This can be done as shown here:

    // Parse it into CSV with a frontend CSV library.
    
    // Then, create a hidden element and automatically download the file.
    var hiddenElement = document.createElement('a');
    hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(csv);
    hiddenElement.target = '_blank';
    hiddenElement.download = 'exported.csv';
    hiddenElement.click();
    

    Source

CherryDT
  • 25,571
  • 5
  • 49
  • 74
SpBills
  • 75
  • 1
  • 6
  • It would be much more straight forward to send the string in memory with the right headers to make it come up as download, without saving to a file first. Not only does the file create a race condition (what if two requests ran in parallel, fighting about the file?) but it also creates an unnecessary I/O overhead. – CherryDT Jul 31 '20 at 21:26
  • @CherryDT I agree in theory, however, I don't see how that's possible with Express. Maybe post your own answer with a solution to this? And, I believe my second solution is still valid. – SpBills Jul 31 '20 at 21:37
  • 1
    `res.attachment('someFileName.csv').send(csv)` (I'm writing one just now) – CherryDT Jul 31 '20 at 21:38