tutorial // Aug 13, 2021

How to Import a CSV Using Next.js and Node.js

How to parse a CSV into a JavaScript array and upload it to a server via fetch and insert it into a MongoDB database.

How to Import a CSV Using Next.js and Node.js

Getting started

For this tutorial, we're going to use the CheatCode Node.js Boilerplate on the server and the CheatCode Next.js Boilerplate on the client.

Starting with the Node.js boilerplate...

Terminal

git clone https://github.com/cheatcode/nodejs-server-boilerplate server

Next, install the boilerplate's dependencies:

Terminal

cd server && npm install

Next, start up the Node.js boilerplate:

Terminal

npm run dev

After the server is running, next, we want to set up the Next.js Boilerplate. In another terminal tab or window, clone a copy:

Terminal

git clone https://github.com/cheatcode/nextjs-boilerplate client

Next, install the boilerplate's dependencies:

Terminal

cd client && npm install

Before starting the boilerplate, we need to install one additional dependency, papaparse which we'll use to help us parse our CSV file:

Terminal

npm i papaparse

Finally, with that, go ahead and start up the boilerplate:

Terminal

npm run dev

With that, we're ready to get started!

Building an Express route to handle uploads

To start, we're going to set up a route using Express (already implemented in the Node.js Boilerplate we just set up) where we'll upload our CSV:

/server/api/index.js

import Documents from "./documents";
import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.use("/uploads/csv", (req, res) => {
    // We'll handle our uploaded CSV here...
    res.send("CSV uploaded!");
  });
};

Inside of the boilerplate, an Express app instance is created and passed into a series of functions in /server/index.js. More specifically, by default we have two functions that consume the app instance: middleware() and api(). The former—defined in /middleware/index.js—is responsible for attaching our Express middleware functions (code that runs before each request received by our Express server is handed off to our routes). The latter—defined in /api/index.js—handles attaching our data-related APIs (by default, a GraphQL server).

In that file, above, beneath the call to set up our graphql() server (we won't be using GraphQL in this tutorial so we can ignore this), we're adding a route to our app instance via the .use() method on that instance. As the first argument, we pass the URL in our app where we'll send a POST request from the browser containing our CSV data.

By default, the boilerplate starts on port 5001, so we can expect this route to be available at http://localhost:5001/uploads/csv. Inside of the callback for the route, though we won't expect anything in return on the client, to ensure the request doesn't hang we respond with res.send() and a brief message acknowledging a successful upload.

/server/api/index.js

import Documents from "./documents";
import graphql from "./graphql/server";
import generateId from "../lib/generateId";

export default (app) => {
  graphql(app);

  app.use("/uploads/csv", (req, res) => {
    const documentsFromCSV = req?.body?.csv;

    for (let i = 0; i < documentsFromCSV.length; i += 1) {
      Documents.insertOne({
        _id: generateId(),
        ...(documentsFromCSV[i] || {}),
      });
    }

    res.send("CSV uploaded!");
  });
};

Adding the functionality we're really after, above, we've added in two big things:

  1. An expectation of some documentsFromCSV being passed to us via the csv field on the req.body (POST request body).
  2. A loop over those documentsFromCSV, adding each one to a MongoDB collection we've imported up top called Documents (the definition for this is included in the Node.js boilerplate for us as an example).

For each iteration of the loop—this will run five times as our test .csv file will be five rows long—we call to Documents.insertOne(), passing an _id set equal to a call to the included generateId() function from /server/lib/generateId.js (this generates a unique, random hex string of 16 characters in length).

Next, we use the JavaScript ... spread operator to say "if there is an object in the documentsFromCSV array at the same position—index—as the current value of i, return it and 'unpack' its contents onto the object alongside our _id (the document we'll ultimately insert into the database)." If for some reason we do not have a document, we fall back to an empty object with || {} to avoid a runtime error. Alternatively (and preferably, if your data may or may not be consistent), we could wrap the call to Documents.insertOne() in an if statement verifying this before we even call it.

That's it for the server. Next, let's jump down to the client and see how to handle parsing our CSV file and getting it uploaded.

Wiring up a React component to parse and upload our CSV

Now, on the client, we're going to set up a React component with a file input that will allow us to select a CSV, parse it into a JavaScript object, and then upload it to the endpoint we just defined on the server.

/client/pages/upload/index.js

import React, { useState } from "react";

const Upload = () => {
  const [uploading, setUploading] = useState(false);

  const handleUploadCSV = () => {
    // We'll handle our CSV parsing and upload here...
  };

  return (
    <div>
      <h4 className="page-header mb-4">Upload a CSV</h4>
      <div className="mb-4">
        <input disabled={uploading} type="file" className="form-control" />
      </div>
      <button
        onClick={handleUploadCSV}
        disabled={uploading}
        className="btn btn-primary"
      >
        {uploading ? "Uploading..." : "Upload"}
      </button>
    </div>
  );
};

Upload.propTypes = {};

export default Upload;

Here, we're using the function component pattern in React to define a component called Upload. Because we're using Next.js (a framework built around React), we're defining our component in the /pages folder, nested under its own folder at /pages/upload/index.js. By doing this, Next.js will automatically render the component we're defining above in the browser when we visit the /upload route (the boilerplate starts on port 5000 by default so this will be available at http://localhost:5000/upload).

Focusing on the return value inside of the Upload function—again, this is a function component, so nothing more than a JavaScript function—we're returning some markup that will represent our component. Because the boilerplate uses the Bootstrap CSS framework, here, we've rendered some basic markup to give us a title, a file input, and a button that we can click to start an upload styled using that framework's CSS.

Focusing on the useState() function being called at the top of our component, here, we're setting a state value that will be used to control the display of our input and button when we're uploading a file.

When calling useState(), we pass it a default value of false and then expect it to return us a JavaScript array with two values: the current value and a method for setting the current value. Here, we use JavaScript array destructuring to allow us to assign variables to these elements in the array. We expect our current value in position 0 (the first item in the array), and we've assigned it to the variable uploading here. In position 1 (the second item in the array), we've assigned the variable setUploading (we expect this to be a function that will set our uploading value).

Down in the return value, we can see uploading being assigned to the disabled attribute on our <input /> as well as our <button />. When uploading is true, we want to disable the ability to select another file or click the upload button. In addition to this, to add context for our users, when uploading is true, we want to change the text of our button to "Uploading..." and when we're not uploading to "Upload."

With all of that in place, next, let's look at the handleUploadCSV function we've stubbed out near the middle of our component. Of note, notice that we're calling to this function whenever our <button /> is clicked.

Parsing and uploading our CSV file

Now for the fun part. Let's flesh out that handleUploadCSV function a bit and get this working.

/client/pages/upload/index.js

import React, { useState, useRef } from "react";
import Papa from "papaparse";

const Upload = () => {
  const [uploading, setUploading] = useState(false);
  const inputRef = useRef();

  const handleUploadCSV = () => {
    setUploading(true);

    const input = inputRef?.current;
    const reader = new FileReader();
    const [file] = input.files;

    reader.onloadend = ({ target }) => {
      const csv = Papa.parse(target.result, { header: true });
    };

    reader.readAsText(file);
  };

  return (
    <div>
      <h4 className="page-header mb-4">Upload a CSV</h4>
      <div className="mb-4">
        <input ref={inputRef} disabled={uploading} type="file" className="form-control" />
      </div>
      <button
        onClick={handleUploadCSV}
        disabled={uploading}
        className="btn btn-primary"
      >
        {uploading ? "Uploading..." : "Upload"}
      </button>
    </div>
  );
};

Upload.propTypes = {};

export default Upload;

We've added quite a bit of detail; let's walk through it. First, when we call to upload our CSV, the first thing we want to do is temporarily disable our <input /> and <button />, so we call to setUploading() passing in true (this will trigger a re-render in React automatically, making our input and button temporarily inaccessible).

Next, in order to get access to the file selected by our user, we've added something special to our component. In React, while we can technically access elements rendered to the DOM using traditional methods like document.querySelector(), it's better if we use a convention called refs.

Refs—short for references—are a way to give ourselves access to a particular DOM element as its rendered by React via a variable. Here, we've added the function useRef() to our react import up top and just beneath our call to useState() have defined a new variable inputRef set to a call to useRef().

With that inputRef, down in our return value, we assign a ref attribute to our <input /> element, passing in the inputRef variable. Now, automatically, when React renders this component, it will see this ref value and assign inputRef back to the DOM node it renders.

Back in handleUploadCSV, we put this to use by calling to inputRef?.current. Here, current represents the currently rendered DOM node (literally, the element as it's rendered in the browser). The inputRef? part is just saying "if inputRef is defined, give us its current value (shorthand for inputRef && inputRef.current)."

With that stored in a variable, next, we create an instance of the native FileReader() class (native meaning it's built-in to the browser and there is nothing to install). Like the name hints, this will help us to manage actually reading the file our user selects via our <input /> into memory.

With our reader instance, next, we need to get access to the DOM representation of our file, so we call to input (containing our DOM node) and access its files property. This contains the file selected by the user in an array, so here, we use JavaScript array destructuring again to "pluck off" the first item in that array and assign it to the variable file.

Next, down at the bottom of our function, notice that we're making a call to reader.readAsText(file). Here, we're telling our FileReader() instance to load the file our user selected into memory as plain text. Just above this, we add a callback function .onloadend which is automatically called by reader once it's "read" the file into memory.

Inside of that callback, we expect to get access to the JavaScript event representing the onloadend event as the first argument passed to the callback function. On that event object, we expect a target attribute which itself will contain a result attribute. Because we asked the reader to read our file as plain text, we expect target.result to contain the contents of our file as a plain text string.

Finally, utilizing the Papa object we imported via the papaparse package we installed earlier, we call the .parse() function passing two arguments:

  1. Our target.result (the plain text string containing our .csv file's contents).
  2. An options object for papaparse which sets the header option to true which is interpreted by the library as expecting the first row in our CSV to be the column titles we want to use as object properties in the objects generated by papaparse (one per row in our CSV).

We're almost done. Now, with our parsed csv, we're ready to call to our server and get this uploaded.

Uploading our CSV to the server

Last part. Let's spit out all of the code and step through it:

/client/pages/upload/index.js

import React, { useState, useRef } from "react";
import Papa from "papaparse";
import pong from "../../lib/pong";

const Upload = () => {
  const [uploading, setUploading] = useState(false);
  const inputRef = useRef();

  const handleUploadCSV = () => {
    setUploading(true);

    ...

    reader.onloadend = ({ target }) => {
      const csv = Papa.parse(target.result, { header: true });

      fetch("http://localhost:5001/uploads/csv", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          csv: csv?.data,
        }),
      })
        .then(() => {
          setUploading(false);
          pong.success("CSV uploaded!");
        })
        .catch((error) => {
          setUploading(false);
          console.warn(error);
        });
    };

    reader.readAsText(file);
  };

  return (...);
};

Upload.propTypes = {};

export default Upload;

To do our upload, we're going to use the built-in browser fetch() function. Remember that earlier in the tutorial, we set up our route on the server at /uploads/csv and suggested that it will be available at http://localhost:5001/uploads/csv. Here, we continue with that assumption, passing that as the URL for our fetch() request.

Next, as the second argument to fetch(), we pass an options object describing the request. Because we want to send our data in the body of our request, we set the HTTP method field to POST. Next, we set the Content-Type header to application/json to let our server know that our request body contains data in a JSON format (if you're curious, this tells our bodyParser middleware at /server/middleware/bodyParser.js how to convert the raw body data before it hands it off to our routes).

Now, for the important part, to the body property we pass an object to JSON.stringify()fetch() expects us to pass our request body as a string—and on that object, we set the csv property we've anticipated on the server, equal to the csv.data property. Here, csv represents the response we received from Papa.parse() and data contains the array of rows in our CSV parsed as JavaScript objects (remember on the server, we loop over this array).

Finally, because we expect fetch() to return us a JavaScript Promise, we add two callback functions .then() and .catch(). The former handling the "success" state if our upload is successful and the latter handling any errors that might occur. Inside of .then(), we make sure to setUploading() to false to make our <input /> and <button /> accessible again and use the pong library included in the boilerplate to display an alert message when our upload is successful. In the .catch(), we also setUploading() to false and then log out the error to the browser console.

Done! Now, when we select our CSV file (grab a test file here on Github if you don't have one) and click "Upload," our file will be parsed, uploaded to the server, and then inserted into the database.

Wrapping up

In this tutorial, we learned how to built a React component with a file input that allowed us to select a .csv file and upload it to the server. To do it, we used the HTML5 FileReader API in conjunction with the papaparse library to read and parse our CSV into a JavaScript object.

Finally, we used the browser fetch() method to hand that parsed CSV off to the server where we defined an Express route that copied our CSV data into a MongoDB database collection.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode