Tutorial: Creating a CSV Download Feature with Postgres, Express, Node, React, and TypeORM for universal browsers

Photo by Sigmund on Unsplash

Tutorial: Creating a CSV Download Feature with Postgres, Express, Node, React, and TypeORM for universal browsers

In this tutorial, we will create a CSV download feature using Postgres as the database, Express and Node.js for the backend, and React for the frontend. We will use TypeORM as the Object-Relational Mapper (ORM) for database interaction.

Prerequisites

  • Latest version of Node.js installed

  • Postgres database installed, along with the PgAdmin4 toolFirst, we will configure our pgAdmin to access our local machine Postgres to make a db keep user data in it and visualize it.

Step 1: Configure pgAdmin for Database Access

  1. Open pgAdmin4 tool.

  2. Create a new server by providing the necessary connection details for your local Postgres instance.

  1. Save the server configuration.

Now you can see your newly created server in the left nav bar where you can create new databases in that server and also write SQL queries for that server.

Step 2: Set Up Backend APIs using Express and Node.js

  1. Create a directory for the backend APIs and initialize a new npm package.
mkdir ems-backend
cd ems-backend
npm init
  1. Install the required npm packages.
npm install @types/cors @types/express cors express nodemon pg reflect-metadata ts-node typeorm typescript

Your package.json will look something like this

//package.json
{
  "name": "ems-backend",
  "version": "1.0.0",
  "description": "crud apis for employees",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon index.ts",
    "tsc": "tsc "
  },
  "author": "devrath",
  "license": "ISC",
  "dependencies": {
    "@types/cors": "^2.8.14",
    "@types/express": "^4.17.18",
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "nodemon": "^3.0.1",
    "pg": "^8.11.3",
    "reflect-metadata": "^0.1.13",
    "ts-node": "^10.9.1",
    "typeorm": "^0.3.17",
    "typescript": "^5.2.2"
  }
}
  1. Create a TypeScript configuration file (tsconfig.json) with the necessary settings.
// tsconfig.json
{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig to read more about this file */

    /* Projects */
    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */

    /* Language and Environment */
    // "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
    "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
    "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
    // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */

    /* Modules */
    // "module": "commonjs",                                /* Specify what module code is generated. */
    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
    // "moduleResolution": "node10",                     /* Specify how TypeScript looks up a file from a given module specifier. */
    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
    // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
    "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
    // "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
    // "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
    // "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
    // "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
    // "resolveJsonModule": true,                        /* Enable importing .json files. */
    // "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */

    /* JavaScript Support */
    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */

    /* Emit */
    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
    // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
    // "removeComments": true,                           /* Disable emitting comments. */
    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */

    /* Interop Constraints */
    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
    // "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
    // "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
    // "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */

    /* Type Checking */
    // "strict": true,                                      /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
    // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */

    /* Completeness */
    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
    // "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  }
}
  1. Create a connection file (connection.ts) to establish a database connection using TypeORM.
// connection/connection.ts

import { users } from "../entities/users";
import { createConnection } from "typeorm";

export const connection = createConnection({
  type: "postgres" ,
  host: "localhost",
  port: 5432,
  username: "postgres",
  password: "password@123",
  database: "sample",
  entities: [users],
  synchronize: true,
  logging: false
});
  1. Define a user entity (users.ts) to represent the user table in the Postgres database.
// entities/users.ts
import {Entity, Column, PrimaryGeneratedColumn} from "typeorm";
@Entity()
export class users {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column()
    email: string;

    @Column()
    isActive: number;
}
  1. Implement CRUD APIs using Express for user management.
// index.ts
import * as express from "express";
import { connection } from "./connection/connection";
import { users } from "./entities/users";
import * as cors from "cors";

const EMPLOYEE_REQUIRED_FIELDS_ARRAY_FOR_CSV = {
  name: "EMPLOYEE NAME",
  id: "EMPLOYEE ID",
  email: "EMAIL",
  isActive: "STATUS",
};

const EMPLOYEE_FORMATTING_FIELD_ARRAY_FOR_CSV = {
  isActive: {
    0: "In Active",
    1: "Active",
  },
};

const HTTP_STATUS_CODES = {
  HTTP_SUCCESS: 200,
  HTTP_FAILURE: 403,
};

//create local server in port 3001
const app = express();
app.use(cors());
app.use(express.json());


const server = app.listen(3001, () => {
  console.log("server running at 3001....");
});


app.get("/api", (req, res) => {
  res.send("Welcome to API");
});

/*
this function will return formatted data 
required to download csv
*/

const generateCsvFile = (
  data,
  requiredFieldsArray,
  formattingFieldArray = {}
) => {
  const csvArr = [];
  const header = Object.values(requiredFieldsArray).join(",");
  csvArr.push(header);
  for (let i = 0; i < data.length; i++) {
    const row = {};
    for (const [key, value] of Object.entries<any>(requiredFieldsArray)) {
      row[value] = data[i][key].toString();
      if (
        !!formattingFieldArray.toString() &&
        formattingFieldArray.hasOwnProperty(key)
      ) {
        row[value] = formattingFieldArray[key][data[i][key]].toString();
      }
    }
    const dataVal = Object.values(row);
    dataVal[0] = "\n" + dataVal[0];
    const newVal = dataVal.join(",");
    csvArr.push(newVal);
  }
  return csvArr;
};

connection
  .then(async (connection) => {
    console.log("connected");
    const usersRepository = connection.getRepository(users);

    // API to get list of users
    app.get("/api/users", async (req, res) => {
      const users = await usersRepository.find();
      res.send(users);
    });

    /*
     API endpoint to trigger CSV download with user data in
     the desired format.
    */
    app.get("/api/users/download", async (req, res) => {
      const resObject = {
        status: HTTP_STATUS_CODES["HTTP_SUCCESS"],
        fileName: `EMPLOYEES_LIST_${new Date().getTime()}.csv`,
        message: "success",
        data: [],
      };
      try {
        const users = await usersRepository.find();
        if (Array.isArray(users) && !!users.length) {
          resObject["data"] = generateCsvFile(
            users,
            EMPLOYEE_REQUIRED_FIELDS_ARRAY_FOR_CSV,
            EMPLOYEE_FORMATTING_FIELD_ARRAY_FOR_CSV
          );
        } else {
          resObject["status"] = HTTP_STATUS_CODES["HTTP_FAILURE"];
          resObject["message"] = "CSV not generated";
        }
      } catch (error) {
        resObject["status"] = HTTP_STATUS_CODES["HTTP_FAILURE"];
        resObject["message"] = error.toString() || "CSV not generated";
      }
      res.send(resObject);
    });

    //API to create new user
    app.post("/api/users", async (req, res) => {
      const user = await usersRepository.create(req.body);
      const results = await usersRepository.save(user);

      res.json({
        message: "success",
        payload: results,
      });
    });

    //API to get particular user detail based on id
    app.get("/api/users/:id", async (req: any, res) => {
      const user = await usersRepository.findOne({
        where: { id: req.params.id },
      });
      res.json({
        message: "success",
        payload: user,
      });
    });

    //API to delete user based on id
    app.delete("/api/users/:id", async (req, res) => {
      const user = await usersRepository.delete(req.params.id);
      res.json({
        message: "success",
      });
    });

    //API to update user data based on id
    app.put("/api/users/:id", async (req: any, res) => {
      const user = await usersRepository.findOne({
        where: { id: req.params.id },
      });
      usersRepository.merge(user, req.body);
      const result = await usersRepository.save(user);
      res.json({
        message: "success",
        payload: result,
      });
    });
  })
  .catch((error) => {
    console.log(error);
  });

Start your backend server and test your crud APIs in Postman.

npm start

Step 4: Test APIs using Postman

Test the implemented APIs using Postman, including creating a new user, getting a list of all users, downloading user data in CSV format, getting user details, deleting a user, and updating a user.

Post API to create a new user

Get API to get a list of all users

Download Api which will return users' data in a format which can be used to download users in Frontend.

Get user details API

Delete a user API

Update a user

Directory Structure

ems-backend/
  |- connection/
  |   |- connection.ts
  |- entities/
  |   |- users.ts
  |- index.ts
  |- tsconfig.json
  |- package.json
  |- ...

We've set up the backend APIs using Express and Node.js, connected to a PostgreSQL database using TypeORM. We've also tested the APIs using Postman to ensure they work as expected.

Now we will be creating a CSV Download Feature in React with TypeScript

We'll set up a React application, configure Tailwind CSS, and implement the CSV download functionality.

Step 1: Create a React App

Let's start by creating a new React application with TypeScript support using Create React App:

npx create-react-app ems-fe --typescript
cd ems-fe

Step 2: Configure TypeScript

Edit the tsconfig.json file to configure TypeScript options:

//tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
    "src"
  ]
}

Step 3: Install and Configure Tailwind CSS

Next, install Tailwind CSS and initialize its configuration:

npm install -D tailwindcss
npx tailwindcss init

We will have below the tailwind config file

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Modify your index.css file to enable Tailwind CSS classes:

@tailwind base;
@tailwind components;
@tailwind utilities;

Step 4: Install Required Libraries

Install the necessary libraries for our project:

npm install @tanstack/react-table axios react-toastify

Your package.json file will look as below

// package.json
{
  "name": "ems-fe",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@tanstack/react-table": "^8.10.3",
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.18.55",
    "@types/react": "^18.2.24",
    "@types/react-dom": "^18.2.8",
    "axios": "^1.5.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "react-toastify": "^9.1.3",
    "typescript": "^4.9.5",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Step 5: Environment Configuration

Create a .env file to store environment variables:

We should always maintain our sensitive strings and variables in the .env file and never commit it in our git versioning or keep it in .gitignore.

// .env
REACT_APP_ENV=
REACT_APP_BASE_URL="http://localhost:3001/api"
REACT_APP_BASE_URL_TWO=

Step 6: Build the Frontend Components

Now, let's create the frontend components needed for our CSV download feature.

App Component

Create a core App component and import it into the index.ts file. This will have the main global configurations, stylings, Providers etc. imported and defined.

// src/App.tsx
import * as React from "react";
import HocLoader from "./components/hoc";
import MyComp from "./components/common/MyComp";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

export const LoaderContext = React.createContext({
  loader: false,
  setLoader: () => {},
});

function App() {
  const [loader, setLoader]: any = React.useState(false);

  return (
    <LoaderContext.Provider
      value={{
        loader,
        setLoader,
      }}
    >
      <ToastContainer />
      <HocLoader Component={<MyComp />} isLoading={loader} />
    </LoaderContext.Provider>
  );
}

export default App;

Loader Higher-Order Component (HOC)

Create a loader higher-order component to handle loading states:

//src/components/hoc/index.tsx
import React from "react";
import { Loader } from "../common/Loader/Loader";

const HocLoader = ({ Component, isLoading = false }: any) => {
  return (
    <>
      {isLoading ? <Loader classAdd="common-loader" /> : ""}
      {Component}
    </>
  );
};

export default HocLoader;

Loader Component

Design a loader component with styles:

// src/components/common/Loader/Loader.tsx
import type { ReactElement } from "react";
import React from "react";
import "./style.css";

export const Loader = ({ classAdd = "" }: any): ReactElement => {
  return (
    <div
      className={`flex h-full w-full grow items-center justify-center ${classAdd}`}
    >
      <div className="h-fit rounded-full bg-indigo-600 p-5">
        <svg
          className="h-10 w-10 animate-spin text-white"
          xmlns="http://www.w3.org/2000/svg"
          fill="none"
          viewBox="0 0 24 24"
        >
          <circle
            className="opacity-25"
            cx="12"
            cy="12"
            r="10"
            stroke="currentColor"
            strokeWidth="4"
          ></circle>
          <path
            className="opacity-75"
            fill="currentColor"
            d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
          ></path>
        </svg>
      </div>
    </div>
  );
};
/* src/components/common/Loader/style.css */
.common-loader {
  position: absolute;
  background: rgba(128, 128, 128, 0.39);
  z-index: 9999;
}

User Data Display and Download Button

Implement a component to display user data in the table using TanStack Table and a download button:

// src/components/common/MyComp.tsx
import * as React from "react";
import {
  createColumnHelper,
  flexRender,
  getCoreRowModel,
  useReactTable,
} from "@tanstack/react-table";
import { createQueryParams, exportCsv } from "../../utils/Utils";
import { apiConstants } from "../../utils/services/api/apiEndpoints";
import { apiCall } from "../../utils/services/api/api";
import { LoaderContext } from "../../App";

type Person = {
  id: number;
  name: string;
  isActive: number;
  email: string;
};

const columnHelper = createColumnHelper<Person>();

const columns = [
  columnHelper.accessor("id", {
    header: () => "User Id",
  }),
  columnHelper.accessor((row) => row.name, {
    id: "name",
    header: () => <span>Name</span>,
  }),
  columnHelper.accessor("isActive", {
    header: () => <span>Status</span>,
    cell: (props) => (
      <span>{props.getValue() === 1 ? "Active" : "In Active"}</span>
    ),
  }),
  columnHelper.accessor("email", {
    header: "Email",
  }),
];

const MyComp = () => {
  const { setLoader } = React.useContext(LoaderContext);
  const [data, setData] = React.useState<Person[]>(() => []);

  React.useEffect(() => {
    getEmployeesList();
  }, []);

  const getEmployeesList = async () => {
    const result = await apiCall(
      apiConstants["getEmployeesList"],
      {
        loader: true,
      },
      setLoader
    );
    setData(result);
  };

  //Download button click handler
  const handleDownloadCsv = async () => {
    exportCsv(
      apiConstants["downloadEmployeesList"],
      setLoader,
      createQueryParams({})
    );
  };

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <div className="flex justify-center items-center min-h-[100vh]">
      <div>
        <div className="flex justify-between items-center mb-10">
          <h1>Users</h1>
          <button
            onClick={handleDownloadCsv}
            className="border p-3 rounded bg-indigo-500 text-orange-100"
          >
            Download
          </button>
        </div>
        <table>
          <thead>
            {table.getHeaderGroups().map((headerGroup) => (
              <tr key={headerGroup.id}>
                {headerGroup.headers.map((header) => (
                  <th className="border p-10" key={header.id}>
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                  </th>
                ))}
              </tr>
            ))}
          </thead>
          <tbody>
            {table.getRowModel().rows.map((row) => (
              <tr key={row.id}>
                {row.getVisibleCells().map((cell) => (
                  <td className="border p-10" key={cell.id}>
                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </td>
                ))}
              </tr>
            ))}
            {!!!data.length && (
              <tr>
                <td colSpan={4} className="border p-10 text-center">
                  No data found
                </td>
              </tr>
            )}
          </tbody>
        </table>
      </div>
    </div>
  );
};

export default MyComp;

Step 7: Configuration and Constants

Create configuration and constants files for your application:

  • src/utils/config/Config.ts

  • src/utils/constants/Constants.ts

//src/utils/config/Config.ts
export const config = Object.freeze({
  appName: 'ems_user',
  env: process.env.REACT_APP_ENV,
  baseUrl: process.env.REACT_APP_BASE_URL,
  baseUrlTwo: process.env.REACT_APP_BASE_URL_TWO,
})
//src/utils/constants/Constants.ts
export const constants = {
  HTTP_SUCCESS_STATUS_CODE: 200,
  csvDownloadedText: "CSV generate successfully",
  somethingWentWrongMessage: "Something went wrong",
};

Step 8: Utility Functions

Define utility functions, such as CSV export, in a Utils.ts file:

// src/utils/Utils.ts
import { notify } from "./services/notify/notify";
import { apiCall } from "./services/api/api";
import { constants } from "./constants/Constants";

// Function to format payload sent in REST APIs
export const createQueryParams = (Obj: any) => {
  const queryParams: any = {};
  Object.keys(Obj).forEach((key: any) => {
    const str: keyof typeof Obj = key;
    if (
      Obj[str] ||
      typeof Obj[str] === "boolean" ||
      typeof Obj[str] === "number"
    ) {
      queryParams[key] = Obj[str];
    }
  });
  return queryParams;
};

//This function generates csv file and downloads it
export const exportCsv = async (
  apiEndpoint = "",
  setLoader = () => {},
  queryParams = {},
  fileName = ""
) => {
  const downloadFileDetails = await apiCall(
    apiEndpoint,
    {
      queryParams,
      loader: true,
    },
    setLoader
  );
  if (downloadFileDetails?.status === constants.HTTP_SUCCESS_STATUS_CODE) {
    const blob = new Blob([downloadFileDetails?.data], { type: "text/csv" });
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.setAttribute("href", url);
    a.setAttribute("download", downloadFileDetails?.fileName || fileName);
    a.click();
    notify.success(downloadFileDetails?.message || constants.csvDownloadedText);
    return;
  }
  notify.error(
    downloadFileDetails?.message || constants.somethingWentWrongMessage
  );
  return;
};

Step 9: Configure React-Toastify

Set up toast notifications using React-Toastify:

//src/utils/services/notify/notify.ts
import { toast } from 'react-toastify';

const options: any = {
  position: "top-center",
  autoClose: true,
  hideProgressBar: true,
  closeOnClick: true,
  pauseOnHover: true,
  draggable: true,
  progress: undefined,
  theme: "light"
}

export const notify = {
  success: (message: any) => { toast.success(message, { ...options }) },
  error: (message: any) => { toast.error(message, { ...options }) },
  warn: (message: any) => { toast.warn(message, { ...options }) },
  info: (message: any) => { toast.info(message, { ...options }) }
}

Step 10: API Service

Create a file to handle API calls:

// src/utils/services/api/api.ts
import { apiEndpoints } from "./apiEndpoints";
import {
  axiosInstances,
  handleError,
  handleResponse,
  injectParams,
  injectQueryParams,
} from "./apiHelper";

interface apiOptionsInterface {
  body?: any;
  params?: any;
  queryParams?: any;
  loader?: boolean;
}

export const apiCall = (
  apiEndpointName: string,
  options?: apiOptionsInterface,
  setLoader = () => {}
) => {
  const { body, params, queryParams, loader = false } = { ...options };

  if (apiEndpoints[apiEndpointName] === undefined) {
    const err = new Error(
      "API " +
        apiEndpointName +
        " not found in endpointConfig. Please check api. Endpoints"
    );
    return err;
  }

  const config = JSON.parse(JSON.stringify(apiEndpoints[apiEndpointName]));

  //Inject params here.
  if (params) {
    config.url = injectParams(config.url, params);
  }
  //Inject params here.
  if (queryParams) {
    config.url = injectQueryParams(config.url, queryParams);
  }
  const axiosInstance = axiosInstances[config.instance] || axiosInstances.i1;
  showHideLoader(loader, true, setLoader);
  return axiosInstance({ method: config.method, url: config.url, data: body })
    .then((res: any) => {
      showHideLoader(loader, false, setLoader);
      return handleResponse(res);
    })
    .catch((err: any) => {
      showHideLoader(loader, false, setLoader);
      return handleError(err);
    });
};

const showHideLoader = (loader: boolean, show: boolean, setLoader: any) => {
  if (loader) {
    show ? setLoader(true) : setLoader(false);
  }
};

const allInstances = Object.keys(axiosInstances);
allInstances.forEach((i) => {
  // Add a request interceptor
  axiosInstances[i].interceptors.request.use(
    function (config: any) {
      // Do something before request is sent
      return config;
    },
    function (error: any) {
      // Do something with request error
      return Promise.reject(error);
    }
  );

  // Add a response interceptor
  axiosInstances[i].interceptors.response.use(
    function (response: any) {
      // Any status code that lie within the range of 2xx cause this function to trigger
      // Do something with response data
      return response;
    },
    function (error: any) {
      // Any status codes that falls outside the range of 2xx cause this function to trigger
      // Do something with response error
      return Promise.reject(error);
    }
  );
});

Step 11: Define API Endpoints

Centralize your API endpoints in one file:

// src/utils/services/api/apiEndpoints.ts
export const apiEndpoints: any = {
  downloadEmployeesList: {
    url: "/users/download",
    method: "get",
    // instance: 'i2'
  },
  getEmployeesList: {
    url: "/users",
    method: "get",
    // instance: 'i2'
  },
};

export const apiConstants: any = Object.keys(apiEndpoints).reduce((cb, iv) => {
  return { ...cb, [iv]: iv };
}, {});

Step 12: API Helper Functions

Build helper functions related to API calls:

// src/utils/services/api/apiHelper.ts
import axios from "axios";
import { config } from "../../config/Config";
import { notify } from "../notify/notify";

export const axiosInstances: any = {
  i1: axios.create({
    baseURL: config.baseUrl,
  }),
  i2: axios.create({
    baseURL: config.baseUrlTwo,
  }),
};

export const injectParams = (_url_: string, params: any) => {
  let url = _url_;
  for (let i in params) {
    url = url.replace(":" + i, params[i]);
  }
  return url;
};

export const injectQueryParams = (_url_: string, queryParams: any) => {
  let url = _url_ + "?";
  Object.keys(queryParams).forEach((qp, index) => {
    url = `${url}${index === 0 ? "" : "&"}${qp}=${queryParams[qp]}`;
  });
  return url;
};

export const handleResponse = (response: any) => {
  if (response.results) {
    return response.results;
  }
  if (response.data) {
    return response.data;
  }
  return response;
};

export const handleError = (error: any) => {
  if (error.response?.status === 401) {
    notify.error("UNAUTHORIZED");
  }
  if (error.data) {
    return error.data;
  }
  if (error.response) {
    return error.response.data || error.response;
  }
  return error;
};

Step 13: Start the React App

Start your React application:

npm start

That's it! You've successfully created a React application with a CSV download feature and backed in Express. You can now customize and extend this application to suit your needs.

We hope you find this tutorial helpful for adding CSV download functionality to your React projects.