Use JavaScript Blobs to Create Files: The Case of Hundreds of Bulk Import Errors

Recently, I had the need to re-write some data bulk upload validation code. The code was re-written so errors were clearly specified in the server endpoint's HTTP response.

The normal approach here would be to show a table that is built off of the list of received errors, but what if the list can go to the hundreds of errors? This was my case and I felt a regular pop-up showing the errors was a little too inconvenient. Instead, I wanted to provide more options to the end user.

Enter Blob: Create a File in the Client

There is this class called Blob in JavaScript that can be used to prepare a file with whichever content you may need. The constructor takes an array of data and an object with options. It is really simple to use as the following example demonstrates:

const myTextFileBlob = new Blob(['Hello, Blob!'], { type = 'text/plain', endings = 'native' });

Just like that, we have created a plain text file with a single line of text reading Hello, Blob!.

As you can probably infer, this can be extended to create pretty much whichever file you need.

In my case, I wanted to create an HTML report file and a CSV file out of the error list. The next section explains how this can be done, and further down you'll find how to create URL's for them.

Creating HTML and CSV Blobs

My particular case works with an array of error objects with the code, type and message properties. We will use all three properties to create an HTML table in the HTML blob, and the individual record rows in the CSV blob.

The code starts by defining a function that creates the blobs. There is a single function that does this so the array of errors is iterated only once, as opposed to having multiple functions (one function per type of blob). This is a performance consideration and your case may be different. Feel free to make your own mind up as to the approach that you take.

const htmlBlobStart = [
    '<!DOCTYPE html>',
    '<html>',
    '<head>',
    '<title>Error Report</title>',
    '</head>',
    '<body>',
    '<h1>Error Report</h1>',
    '<table>',
    '<thead>',
    '<tr>',
    '<th>Code</th><th>Type</th><th>Message</th>',
    '</tr>',
    '<tbody>'
];

const htmlBlobEnd = [
    '</tbody>',
    '</table>',
    '</body>',
    '</html>',
];

function createErrorBlobs(errors) {
    // One data array per blob type.
    const htmlBlobData = [ ...htmlBlobStart ];
    const csvBlobData = ['Code,Type,Message\n'];
    // Iterate and build the documents.
    errors.forEach(e => {
        htmlBlobData.push(`<tr><td>${e.code}</td><td>${e.type}</td><td>${e.message}</td></tr>`);
        csvBlobData.push(`${e.code},${e.Type},"${e.message}"\n`);
    });
    // The HTML document needs post-data processing.
    htmlBlobEnd.forEach(x => htmlBlobData.push(x));
    // Return the blobs.
    return {
        html: new Blob(htmlBlobData, { type: 'text/html', endings: 'native' }),
        csv: new Blob(csvBlobData, { type: 'text/csv', endings: 'native' })
    };
}

Hopefully, you'll find the above code very simple. It is a bit verbose because of the HTML overhead that is needed to create the document in this format. Note that no styling has been provided and you may include it in any way HTML allows styling. Just add it any way you see fit. In my case, I used a <style> block inside <head> (not shown in the example).

The part most important to this blog article is: Once you have created your data, create blobs with them. This is done in the return statement. At this point, you may be wondering about the options given to the Blob constructor.

The type option is the MIME type of the contents in the blob, and the endings option controls how \n (new line) characters inside the blob's data are consumed: When you specify native, all \n characters are converted to the native line ending of the running Operating System. This way you can create files that are compliant with the user's OS effortlessly.

Ok, great: Now we have blobs. How can we enable the user to consume said blobs? Let's keep moving.

Making Blobs Accessible to the User

This is so simple it might be borderline silly: There is a static function in the URL class called createObjectURL(). This is all we need to create a user-consumable URL for the blobs. All we need to do is add them to an anchor element the user can click on, and voilá!, we are done.

The following code shows how to create two anchor elements with plain JavaScript, but feel free to use your library/framework tools to accomplish the same. You might even want to consider creating a <BlobLink /> component for your project that receives the blob as a prop and encapsulates the creation of the URL's for you.

const blobs = createErrorBlobs(httpResposeData.errors);
const htmlLink = document.createElement('a');
const csvLink = document.createElement('a');
htmlLink.href = URL.createObjectURL(blobs.html);
htmlLink.target = '_blank'; // To open in a new window.
htmlLink.innerText = 'Error Report (opens in a new window)';
csvLink.href = URL.createObjectURL(blobs.csv);
csvLink.download = 'error-report.csv'; // For the user to download this.
csvLink.innerText = 'Download (CSV format)';
// Obtain the links' parent:
const parent = document.getElementById('report-links');
parent.appendChild(htmlLink);
parent.appendChild(csvLink);

Here, we call for the blob-creating function and then create and configure anchor HTML elements with URL's created using URL.createObjectURL(). Finally, the anchor elements are placed in the document by locating the parent by ID.

Clean Up

There is a counterpart function to clean URL's up: URL.revokeObjectURL(). It exists because, as long as the URL exists, the blob is maintained in memory. Use it whenever the user navigates away from the results page. It takes the created URL as its only argument. If you are componetizing this logic, this would probably go in the unmount() (or similarly named) lifecycle event of your component.


Conclusion

Blobs are a great way to provide a user with data in various formats, both for presentation on screen and for consumption in other applications (such as MS Excel) in the form of downloads without requiring a round trip to the application's back-end services.