React GraphQL File Upload with Apollo GraphQL

React GraphQL File Upload with Apollo GraphQL

·

7 min read

File uploads are a crucial aspect of modern web applications, enabling users to share and manage various types of files, such as images, documents, and media. This functionality allows users to interact with applications in more dynamic and versatile ways, whether it's uploading a profile picture, sharing documents with colleagues, or attaching files to messages. Implementing file uploads involves handling the process of transferring files from the client to the server, processing them appropriately, and storing them securely. Techniques such as multipart form-data or GraphQL mutations with file scalars are commonly used to facilitate file uploads in web development. Overall, file uploads enhance the interactivity and utility of web applications, enriching user experiences and expanding the capabilities of online platforms.

You can see the final version of the working React GraphQL upload example here.

Part 1 - Setting Up a File Upload Server with Apollo and Express

In this article, we will guide you through setting up a file upload server using Apollo Server, Express, and GraphQL. This server will handle file uploads, store them locally, and provide a query to retrieve the list of uploaded files.

Project Structure

Before diving into the code, let's outline the structure of our project:

project-root/
├── server/
│   ├── index.js
│   ├── typeDefs.js
│   └── resolvers.js
└── files/

The files directory will be used to store uploaded files. Now, let's walk through each part of the implementation.

Server Setup

First, we set up the main server in server/index.js:

import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import cors from "cors";
import express from "express";
import graphqlUploadExpress from "graphql-upload/graphqlUploadExpress.mjs";
import { resolvers } from "./resolvers.js";
import { typeDefs } from "./typeDefs.js";

const app = express();

// This middleware should be added before calling `applyMiddleware`.
app.use(graphqlUploadExpress());

app.use("/files", express.static("files"));

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

await server.start();

app.use("/graphql", express.json(), cors(), expressMiddleware(server));

await new Promise((r) => app.listen({ port: 5000 }, r));

console.log(`🚀 Server ready at http://localhost:5000/graphql`);

In this code:

  • We import necessary modules and middlewares.

  • We configure graphqlUploadExpress middleware to handle file uploads.

  • We serve static files from the files directory.

  • We create an instance of ApolloServer with our type definitions and resolvers.

  • We start the server and set it to listen on port 5000.

Type Definitions

Next, we define our GraphQL schema in server/typeDefs.js:

export const typeDefs = `#graphql
  scalar Upload

  type File {
    filename: String!
    mimetype: String!
    encoding: String!
  }

  type Query {
    getFiles: [String!]
  }

  type Mutation {
    singleUpload(file: Upload!): File!
  }
`;

Here:

  • We define a custom scalar Upload to handle file uploads.

  • We define a File type to represent uploaded files.

  • We provide a getFiles query to retrieve the list of uploaded files.

  • We provide a singleUpload mutation to handle single file uploads.

Resolvers

Finally, we implement the resolvers in server/resolvers.js:

import { randomBytes } from "crypto";
import {
  createWriteStream,
  existsSync,
  mkdirSync,
  readdirSync,
  unlink,
} from "fs";
import GraphQLUpload from "graphql-upload/GraphQLUpload.mjs";
import path from "path";

export const resolvers = {
  Upload: GraphQLUpload,
  Query: {
    getFiles: async function () {
      if (existsSync("files")) {
        const files = readdirSync("files");
        return files.map((n) => `http://localhost:5000/files/${n}`);
      } else return [];
    },
  },
  Mutation: {
    singleUpload: async function (_root, { file }) {
      const { createReadStream, filename, encoding, mimetype } = await file;
      const stream = createReadStream();

      const uploadPath = "files";
      const uploadFileName = `${randomBytes(6).toString("hex")}_${filename}`;
      const uploadFileUrl = path.join(uploadPath, uploadFileName);

      mkdirSync(uploadPath, { recursive: true });

      const output = createWriteStream(uploadFileUrl);

      stream.pipe(output);

      stream.on("error", (error) => output.destroy(error));

      await new Promise(function (resolve, reject) {
        output.on("close", () => {
          console.log("File uploaded");
          resolve();
        });

        output.on("error", (err) => {
          console.log(err);
          unlink(uploadFileUrl, () => {
            reject(err);
          });
        });
      });

      return { filename, mimetype, encoding };
    },
  },
};

In the resolvers:

  • The Upload scalar is handled by GraphQLUpload.

  • The getFiles query checks if the files directory exists, reads its contents, and returns the URLs of the stored files.

  • The singleUpload mutation handles file uploads by reading the file stream, generating a unique file name, saving the file to the files directory, and returning the file details.

Running the Server

To run the server, execute the following commands in your project root:

pnpm install
node server/index.js

You should see the message 🚀 Server ready at http://localhost:5000/graphql. You can now use this URL to interact with your GraphQL server.

Part 1 Conclusion

In this section, we set up a file upload server using Apollo Server and Express. We defined the necessary GraphQL schema and resolvers to handle file uploads and retrievals. This setup provides a robust foundation for building applications that require file upload capabilities.

Part 2 - Building a File Upload Client with React and Apollo Client

In this article, we will guide you through setting up a client-side application using React and Apollo Client to handle file uploads. This client will interact with the file upload server we previously set up and provide an interface for uploading and retrieving files.

Project Structure

Before diving into the code, let's outline the structure of our client project:

project-root/
├── client/
│   ├── src/
│   │   ├── components/
│   │   │   ├── Files.jsx
│   │   │   └── UploadFile.jsx
│   │   ├── App.jsx
│   │   └── index.jsx
│   └── index.html

Setting Up Apollo Client

First, we set up Apollo Client in client/src/index.jsx:

import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";

// `uploadLink` is a terminating link
const uploadLink = createUploadLink({
  uri: "http://localhost:5000/graphql",
  headers: {
    "Apollo-Require-Preflight": "true",
  },
});

// `uploadLink` is used instead of httpLink
const client = new ApolloClient({
  link: uploadLink,
  cache: new InMemoryCache(),
});

const root = createRoot(document.getElementById("root"));

// client instance is provided to the entire react app using ApolloProvider
// Now we can use apollo hooks (useQuery, useMutation, ...) in child components
root.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

In this code:

  • We import necessary modules from Apollo Client and React.

  • We configure createUploadLink to handle file uploads by setting the GraphQL endpoint.

  • We create an instance of ApolloClient with the upload link and an in-memory cache.

  • We use ApolloProvider to make the Apollo Client available throughout the React app.

Uploading Files

Next, we implement the UploadFile component in client/src/components/UploadFile.jsx:

import { gql, useMutation } from "@apollo/client";
import { useState } from "react";
import { GET_FILES_QUERY } from "./Files";

const SINGLE_UPLOAD_MUTATION = gql`
  mutation SingleUpload($file: Upload!) {
    singleUpload(file: $file) {
      filename
      mimetype
      encoding
    }
  }
`;

function UploadFile() {
  const [file, setFile] = useState(null);
  const [uploadRequest, { loading, error }] = useMutation(
    SINGLE_UPLOAD_MUTATION
  );

  const uploadFile = async () => {
    if (!file) return;
    try {
      const res = await uploadRequest({
        variables: { file },
        refetchQueries: [{ query: GET_FILES_QUERY }],
      });
      if (res.data) {
        setFile(null);
      }
    } catch (err) {
      console.error(err);
    }
  };

  return (
    <div>
      <input
        className="App-input"
        type="file"
        onChange={(e) => setFile(e.target.files[0])}
      />
      <button onClick={uploadFile}>Upload</button>
      <p>{loading && "Uploading..."}</p>
      <p>{error?.message}</p>
    </div>
  );
}

export default UploadFile;

In this component:

  • We define the SINGLE_UPLOAD_MUTATION to handle file uploads.

  • We use useState to manage the selected file.

  • We use useMutation to execute the upload mutation.

  • We provide a file input and a button to trigger the upload process, displaying any loading or error messages.

Displaying Uploaded Files

Finally, we implement the Files component in client/src/components/Files.jsx:

import { gql, useQuery } from '@apollo/client';

export const GET_FILES_QUERY = gql`
  query GetFiles {
    getFiles
  }
`;

function Files() {
  const { data, loading, error } = useQuery(GET_FILES_QUERY);

  if (loading) return <h1>loading files...</h1>;
  if (error) return <p>{error.message}</p>;

  return (
    <div>
      {data?.getFiles?.map((f, i) => (
        <div key={f}>
          <a href={f} target='_blank' rel='noreferrer'>
            File #{i + 1}
          </a>
        </div>
      ))}
      {!data?.getFiles.length && <p>No files uploaded yet</p>}
    </div>
  );
}

export default Files;

In this component:

  • We define the GET_FILES_QUERY to fetch the list of uploaded files.

  • We use useQuery to execute the query and manage loading and error states.

  • We display the list of files as links, handling cases where no files are uploaded yet.

Integrating Components

We integrate these components in our main app file, client/src/App.jsx:

import React from "react";
import UploadFile from "./components/UploadFile";
import Files from "./components/Files";

function App() {
  return (
    <div className="App">
      <h1>File Upload with Apollo Client</h1>
      <UploadFile />
      <Files />
    </div>
  );
}

export default App;

In this code:

  • We import UploadFile and Files components.

  • We render these components within a simple layout.

Running the Client

To run the client, execute the following commands in your project root:

pnpm install
pnpm dev

Your React app should now be running on http://localhost:3000, providing an interface to upload files and view the list of uploaded files.

Part 2 Conclusion

In this section, we set up a client-side application using React and Apollo Client to handle file uploads. We defined components to upload files and display the list of uploaded files. This setup complements the server-side implementation and provides a complete solution for file upload functionality.