Exporting data as CSV file from web apps (with Golang)


While you can certainly export data from javascript with two different ways, they both aren’t good solutions. First one doesn’t allow you to name the downloaded file (it’s just download), second is only supported in Chrome, Firefox and latest version of Microsoft Edge (by the time of writing this post). So the only acceptable option for me is writing a server which accepts POST data (from HTML form, not AJAX), formats it to CSV and than downloads it as a file with logically predefined file name in browser. I implemented this approach in Golang since it’s the language I’m currently learning but it can be easily translated to other server-side languages.

First we create a HTML form with certain action (lets call it /csv-export) and method POST. It contains one hidden input (let’s call it data). When we want to export data from our javascript application, we set this data as the value of hidden input named data and than trigger submit on the form. Something like this (with help of jQuery):

<html>
<body>
<form action="/contacts" method="POST">
    <input id="contactsData" type="hidden" name="data">
</form>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script>
    var data = [
        {email:"john.doe@example.com", open:32, link:33},
        {email:"jana.doe@example.com", open:7, link:13}
    ];
    $('#contactsData').val(JSON.stringify(data));
    $('form').submit();
</script>
</body>
</html>

On server-side we accept the POST data and convert it to data structure which Go knows. Creating a CSV content is pretty easy in Go thanks to built-in CSV package. When CSV is generated we must set an appropriate response headers to indicate this response body is to be downloaded in browser as file and with a predefined name.

So here is the actual implementation in Go step-by-step where only route handler function is displayed (and where gin is used).

type Contact struct {
    Email string
    Open  int64
    Link  int64
}

type Contacts []Contact

func ContactsCSV(c *gin.Context) {
    contactsData := c.PostForm("data")

    var contacts Contacts
    json.Unmarshal([]byte(contactsData), &contacts)

First we retrieve data value from request body. We than parse the data into the data structure known to Go. This data structure is slice of data structures of type Contact. Because data value is JSON string, we use built-in json package, more precisely json.Unmarshal to convert this string to earlier defined data structure.

   b := &bytes.Buffer{}
    w := csv.NewWriter(b)

    if err := w.Write([]string{"email", "opens"}); err != nil {
        log.Fatalln("error writing record to csv:", err)
    }

    for _, contact := range contacts {
        var record []string
        record = append(record, contact.Email)
        record = append(record, strconv.FormatInt(contact.Open, 10))
        if err := w.Write(record); err != nil {
            log.Fatalln("error writing record to csv:", err)
        }
    }
    w.Flush()

    if err := w.Error(); err != nil {
        log.Fatal(err)
    }

A lot of things is happening there. First we create a bytes buffer where CSV content will be appended. We use this buffer as a target for csv.NewWriter. CSV writer work by appending array of strings to specified target (bytes buffer) for each entry. Our first entry is header so we insert it manually. Then we iterate over all contacts and in each iteration we create an array of string which contains desired fields which we want in resulted file ((notice that in example I am not writing Contact.Link to CSV)). Then we flush the data (indicate that writing is over). Good practice in Go is to check for errors every now and then (we check for them after every array of strings is written and after we flush the data).

   c.Header("Content-Description", "File Transfer")
    c.Header("Content-Disposition", "attachment; filename=contacts.csv")
    c.Data(http.StatusOK, "text/csv", b.Bytes())

This is the last and most important thing – preparing response and its headers. Also notice that for Content-Disposition header we set the name of an attachment to contacts.csv. This is the file name user will get when download dialog opens to him after calling an action for exporting the data.