4 min read

REST File Uploads Alongside GraphQL: A Comprehensive Guide

REST File Uploads Alongside GraphQL: A Comprehensive Guide
Photo by Siddhant Kumar / Unsplash

I recently needed to add file upload capabilities to a graphql powered web app. While file uploads are possible, it is not the most ergonomic solution to upload a file directly with a graphql mutation.

The solution that I settled on was a graphql query that provided a short-lived access token coupled with a REST endpoint for the actual file upload; here is why:

Authentication

Most likely, you already have authentication set up for your graphql API (if not, check out this article).

In my specific case, I was not able to use the same authentication token from my graphql API to the upload endpoint due to different subdomains.

An easy way around this is to create a graphql query that returns a simple jwt token; make this token expire in 60 seconds to enhance security.

Here is an example of a GraphQL query that returns a string:

query GetToken {
  token
}

This query assumes that your GraphQL schema has a token field at the root level that returns a string.

After executing this query and getting the token, you can use it in the Authorization header of a fetch call.

const GET_TOKEN = gql`
  query GetToken {
    token
  }
`;

The example provided demonstrates how to execute a GraphQL query that returns a token as a string and then use that token in the Authorization header of a fetch call.

Server Side (Express.js)

Let's start with the server side using Express.js and the express-fileupload middleware.

If your graphql server is integrated with Express.js, this is an easy addition. Just add an additional endpoint that handles file uploads:

const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();

// graphql endpoint
app.post('/graphql, <your graphql server>)


// default options
app.use(fileUpload());

app.post('/upload', function(req, res) {
  let sampleFile;
  let uploadPath;

  if (!req.files || Object.keys(req.files).length === 0) {
    return res.status(400).send('No files were uploaded.');
  }

  // The name of the input field (i.e. "sampleFile") is used to retrieve the uploaded file
  sampleFile = req.files.sampleFile;
  uploadPath = __dirname + '/uploads/' + sampleFile.name;

  // Use the mv() method to place the file somewhere on your server
  sampleFile.mv(uploadPath, function(err) {
    if (err)
      return res.status(500).send(err);

    res.send('File uploaded!');
  });
});

app.listen(8000, function() {
  console.log('App running on port 8000');
});

Client Side (React.js)

On the client side, you can use the fetch API to send a POST request with the file data. Here's a simple React component with file input and a submit button:

import React, { useState } from 'react';

function FileUpload() {
  const [file, setFile] = useState(null);

  const submitFile = async (event) => {
    event.preventDefault();
    const formData = new FormData();
    formData.append('sampleFile', file);

    try {
      const res = await fetch('http://localhost:8000/upload', {
        method: 'POST',
        headers: {
          authorization: <token>
        },
        body: formData,
      });

      if (res.ok) {
        console.log('File uploaded successfully');
      } else {
        console.error('Error uploading file');
      }
    } catch (err) {
      console.error(err);
    }
  };

  const handleFileChange = (event) => {
    setFile(event.target.files[0]);
  };

  return (
    <form onSubmit={submitFile}>
      <input type="file" onChange={handleFileChange} />
      <button type="submit">Upload</button>
    </form>
  );
}

export default FileUpload;

This React component includes a file input and a submit button. When the form is submitted, it sends a POST request to the /upload endpoint on the server with the file data.

FormData is a built-in web API available in modern browsers. It provides a simple way to construct a set of key-value pairs representing form fields and their values. This can be used to send data in a fetch call.

This formData object is then used as the body of a fetch POST request. The server receives these fields just like it would if they were sent from a standard HTML form.

Note that when you use FormData with fetch, you don't need to set the Content-Type header to multipart/form-data; the browser automatically sets it for you, and also takes care of setting the boundary parameter in the Content-Type header, which is used to delineate the individual parts of the request.

Please note that this is a very basic example and does not include error handling or feedback to the user about the upload progress or success/failure of the upload. You would likely want to enhance this for a production application.

The Importance of Using Multipart Form Data

When uploading files in a web application, the multipart/form-data content type should be used. This is not specific to React, but is a standard in web development when dealing with file uploads.

The multipart/form-data content-type is used to send binary data in the body of a request, like the contents of a file. This is necessary when uploading files because the data contained in files is not text, but binary data.

In a multipart/form-data body, each field or file gets its own section in the payload, separated by a boundary. Each section has its own content-disposition header that includes the field name and other information. For files, this header also includes the filename and the file's content type.

The boundary parameter in the Content-Type header for multipart/form-data is used to define a delimiter that separates different parts of the HTTP message body. Each part contains a different set of data.

The boundary is a string that is generated by the client and should be unique to avoid any potential overlap with the data being transmitted. This string is placed between each part of the multipart message, and also at the end of the message to indicate the end of the data.

Here is an example of how it might look:

Content-Type: multipart/form-data; boundary=AaB03x

--AaB03x
Content-Disposition: form-data; name="submit-name"

Larry
--AaB03x
Content-Disposition: form-data; name="files"; filename="file1.txt"
Content-Type: text/plain

... contents of file1.txt ...
--AaB03x--

In this example, AaB03x is the boundary string. Each part of the message is separated by --AaB03x, and the end of the message is indicated by --AaB03x--.

Be careful not to explicitly set the Content-Type header in your fetch request, as it will prevent the browser from adding a boundary string. I recently made this mistake, which led to hours of troubleshooting to find out I needed to leave the header alone!