Step-by-Step: How to Download a CSV File using Angular
By Hassib Moddasser
Last update on Jun 26, 2023 · 6 minutes
Introduction
One of the most common features in business applications is exporting data and downloading it in CSV format. It helps companies organize the data and generate report results or do anything they want.
A CSV file contains data values separated by commas. It has an extension of .csv and can be edited by Spreadsheet programs such as Microsoft Office, Google Sheets, etc. Since CSV files are in plaintext, they're easier to import into other database storage.
In this article, we will learn how the engineering team at Merlino Software Agency implemented exporting data to CSV files in the OneFood application. We will use the product’s model as the case study.
To make it clear and straightforward, I divided this article into the two following sections:
- Front-end: This is implemented using Angular and is responsible for making a GET request to the back-end and saving the data as a CSV file.
- Back-end: This is implemented using ExpressJS and is responsible for querying the data and sending it to the front-end when requested.
Firstly, I will explain what happens on the front-end side, and after that, we will head to the server-side. The case study’s architecture is implemented using Clean Architecture. If you want to know what Clean Architecture is and how to use it in Express applications, I have already written a blog. Feel free to check it here.
Enough talking; let’s get our hands dirty by writing some code.
Getting started
Let's start by installing an NPM package called file-saver, which is a client-side file-saving library and perfect when you generate files on the client.
bash|1# Basic Node.JS installation2npm install file-saver --save3 4# Additional typescript definitions5npm install @types/file-saver --save-dev
This package is built to export client-side-generated content, but our data will come from the server-side. We'll therefore need to address this.
The Front-End Logic
Let's start by creating a button in products.component.html and the function that will call our back-end and save the CSV file on the user's machine.
html|1<!-- products.component.html -->2<button (click)=”downloadCSV()”/>Downlad CSV</button>
In your products.component.ts file instead, we will have the following method, which utilizes the saveAs function from the file-saver package we installed before:
typescript|1// products.component.ts2import {saveAs} from "file-saver";3import ProductService from "../services/product.service.ts";4 5constructor(private _productService: ProductService) {}6 7downloadCSV(): void {8 this._productService.downloadCSV({9 search: this.search,10 sellerId: this.sellerId11 }).subscribe((buffer) => {12 const data: Blob = new Blob([buffer], {13 type: "text/csv;charset=utf-8"14 });15 // you may improve this code to customize the name 16 // of the export based on date or some other factors17 saveAs(data, "products.csv");18 });19}
As you notice, we rely on downloadCSV from ProductService, which attempts to get the data from the server and handles pagination search, etc.
Here's what it looks like:
typescript|1// product.service.ts2/*3 * Download a CSV of products4 *5 * @param params6 * @param params.sort7 * @param params.search8 * @param params.sellerId9 */10downloadCSV(11 params: {12 sort?: string;13 search?: string;14 sellerId?: string;15 } = {}16): Observable<any> {17 const {sort = "-createdAt", search, sellerId} = params;18 return this._httpClient.get("<your-api-base-url>/products/csv", {19 params: {20 ...(sort && {sort}),21 ...(search && {search}),22 ...(sellerId && {sellerId})23 },24 responseType: "text"25 });26}
As you can see, this method makes a GET request to the products/csv endpoint on line 18 and will return data back to the downloadCSV method in the products.component.ts file.
The downloadCSV method receives the data as a buffer from the back-end. By setting the GET method's option responseType to text, we can pass it back to our component as is, constructing then a Blob object with it, which is what file-saver wants. When doing so, let's set type as text/csv as we need a CSV file and set the charset to utf-8 to support all kinds of characters.
After all this, we call the saveAs function of the file-saver package that we already installed at the beginning. That's it; all the data will be downloaded as a CSV file named products.csv in our use-case.
If you want to read more about Blobs, click here.
The Back-End logic
Now that we know how to implement the front-end logic of downloading data in a CSV format using Angular, let's check what happens on the server-side when the downloadCSV method makes a GET request.
Clean Architecture has four concentric circles that represent different areas of software: Entities, Use Cases, Interface Adapters, and Framework & Drivers.
When the HTTP client calls the products/csv endpoint, the request goes to a private route called products which looks like this:
javascript|1// app/routes/api/private/products.js2import express from 'express';3 4import controller from 'controllers/api/productController';5import makeExpressCallback from 'routes/make-callback';6 7const router = express.Router();8 9router.route('/products').get(makeExpressCallback(controller.getAllProducts));10router.route('/products/csv').get(makeExpressCallback(controller.makeGetProductsCSV));11// …12 13export default router;
The products.js file handles many requests related to the product, yet you see the /products/csv route, which is a GET request, and the makeGetProductsCSV function is wrapped in makeExpressCallback and passed to it.
Let's start with the makeGetProductsCSV function implementation, and then we will discuss what the wrapper does:
javascript|1// app/use-cases/api/products/get-products-csv.js2import {asSafe} from "services/common";3import {downloadResource} from "services/utility";4 5export default function makeGetProductsCSV({productsDb}) {6 return async function getProductsCSV({query}) {7 const {search, sort = "-createdAt", sellerId} = query;8 9 const options = {10 sort11 };12 13 let filter = {14 ...(sellerId && {sellerId}),15 deleted: false16 };17 18 if (search && search !== "") {19 const $regex = new RegExp(`.*${search}.*`, "ig");20 filter = {21 ...filter,22 $or: [23 {name: {$regex}}24 // …25 ]26 };27 }28 29 let products = await productsDb.find(filter, options);30 31 // clean up object and get rid on useless fields32 products = asSafe(products);33 34 const fields = [35 {label: "ID", value: "_id"},36 {label: "Product name", value: "name"},37 {label: "Seller name", value: "sellerName"},38 {label: "Description", value: "description"},39 {label: "Categories", value: "category"},40 // …41 ];42 43 return {44 contentType: "text/csv",45 fileName: "product.csv",46 csv: downloadResource(fields, products)47 };48 };49}
As you can notice, in this file we use a method called downloadResource imported from a common utility file. This function is defined below and uses a package that helps us transform JSON to CSV:
javascript|1// services/utility.js2import {Parser} from 'json2csv';3 4/**5 * Transform Mongo list to CSV6 * @param fields This is an array of objects that has a number of7 * ... functionalities which includes, specifying what values should be8 * ... parsed from data sent, giving a custom header to each column, etc.9 * @param data This is an array of objects representing data to be 10 * ... populated in the CSV file11 * @returns {*}12 */13const downloadResource = (fields, data) => {14 const json2csv = new Parser({fields});15 return json2csv.parse(data);16};
Moreover, in the use-case snippet above:
- The search, sort, and sellerId variables were destructured from the query.
- The options and filter objects were defined.
- The product records are fetched from the database according to the options and filter objects’ criteria.
- The unnecessary columns are filtered out from the product records using the asSafe function.
- The fields array was defined, which presents the row and columns.
- The CSV data plus the contentType and fileName properties are returned as an object to the client-side.
Yes, it was that easy.
The makeExpressCallback is a wrapper function that receives the req and res as inputs from the .get() and .post() routes of Express, creates a clean request object with all possible useful information needed by use cases, handles whatever it's returned by controllers (i.e., data or errors), and replied to clients in a standardized way.
Here's what it looks like:
javascript|1// app/routes/make-callback.js2controller(httpRequest)3 .then((httpResponse) => {4 if (httpResponse && httpResponse.contentType === 'application/octet-stream') {5 res.setHeader('content-disposition', `attachment; filename=${httpResponse.fileName}`);6 res.set('Content-Type', 'application/octet-stream');7 res.attachment(httpResponse.fileName);8 res.status(200).send({success: true});9 }10 11 if (httpResponse && httpResponse.contentType === "text/csv" && httpResponse.csv) {12 res.setHeader("content-disposition", `attachment; filename=${httpResponse.fileName}`);13 res.setHeader("Content-Type", "text/csv");14 res.attachment(httpResponse.fileName);15 return res.status(200).send(httpResponse.csv);16 }17 18 res.set('Content-Type', 'application/json');19 res.type('json');20 const body = {21 success: true,22 code: 200,23 language: AcceptLanguage,24 data: httpResponse25 };26 res.status(200).send(body);27 })28 .catch((e) => {29 console.error(e);30 console.log(JSON.stringify(e));31 32 res.status(400).send({33 success: false,34 code: 400,35 language: AcceptLanguage,36 error: {37 description: e?.errors?.customsCode?.message || e.message38 }39 });40 });
If you want to know more about Clean Architecture, I have already written a blog that you can read here.
Conclusion
In the programming world, there might be a hundred ways to solve a problem, but we choose the most efficient solution and implement it in a clean way. I hope that this article helped you to do so.
The Angular part was straightforward to implement, while the back-end might have complicated because it is implemented using Clean Architecture. You can implement your solution for the back-end without using Clean Architecture but trust me: it will take you a bit to familiarize yourself with it and implement it in your next project but, it will make your life so much easier and will help you keep your code clean, structured and tested as you add complexity to it.
If you find this helpful or have any suggestions, please reach me through Twitter. Also, don't forget to share it with your friends, colleagues, and network if you like it.
Talk to you soon!
What do you think about this article?
Whether it was helpful or not, we would love to hear your thoughts! Please leave a comment below.