GraphQL File Upload using React with Apollo GraphQL

·

5 min read

We are going to discuss how to set up a full stack react graphql app which features file upload. We will be using @apollo/client for frontend react and apollo-server-express for backend graphql server. The file upload feature is made available for graphql by two node packages called apollo-upload-client and graphql-upload both of which is provided by jaydenseric

Peek 2021-10-14 11-28.gif

Prerequisites

Before you proceed with this article, you must have some basic knowledge about the following,

  • Handling forms in React
  • Running queries and mutations against graphql server using apollo client

Since this is a full-stack application let's split the article into two,

  1. Backend GraphQL Server
  2. Frontend React

Folder Structure

  • client directory is where our react client sits.
  • server directory is where the backend graphql server sits.

apollo-file-struct.png

Part 1 - Backend GraphQL Server

In this part, we are going to talk about how to deal with files sent from the React client using graphql mutations.

Installation

  • Create server directory and initialize node project
    yarn init -y
    yarn add express apollo-server-express graphql graphql-upload
    

Set up backend graphql server

First of all, let's start with a minimal Express GraphQL boilerplate. Here we create a new apollo-server with typeDefs and resolvers and apply it as middleware to express app instance. And also we bring in graphqlUploadExpress from graphql-upload and apply it as middleware for express app instance.

// server/index.js
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const {
  ApolloServerPluginLandingPageGraphQLPlayground,
} = require('apollo-server-core');
const { graphqlUploadExpress } = require('graphql-upload')

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

  const app = express();

  await server.start();

  server.applyMiddleware({ app });

 app.use(graphqlUploadExpress());

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

  console.log(`🚀 Server ready at http://localhost:5000${server.graphqlPath}`);
}

startServer();

Now we need to fill in details of GraphQL schema in typeDefs and how to resolve each field in the schema using resolvers.

const typeDefs = gql`
  scalar Upload

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

  type Query {
    getFiles: [String!]
  }

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

We have a GraphQL query getFiles which resolves an array of string which specifies URLs to the uploaded files.

const { GraphQLUpload } = require('graphql-upload')

...

const resolvers = {
  Upload: GraphQLUpload,
  Query: {
    getFiles: async function () {
      if (fs.existsSync('files')) {
        const files = fs.readdirSync('files');
        return files.map((n) => `http://localhost:5000/files/${n}`);
      } else return [];
    },
  },
};

The file object that we get from the second parameter of the singleUpload resolver is a Promise that resolves to an Upload type with the following attributes:

  • stream - The upload stream of the file(s) we’re uploading. We can pipe a Node.js stream to the filesystem or other cloud storage locations.
  • filename - The name of the uploaded file(s).
  • mimetype - The MIME type of the file(s) such as text/plain, application/octet-stream, etc.
  • encoding - The file encoding such as UTF-8.

    //server/index.js
    const resolvers = {
    ...
    Mutation: {
      singleUpload: async function (root, { file }) {
        const { createReadStream, filename, encoding, mimetype } = await file;
        const stream = createReadStream();
    
        fs.mkdirSync(path.join(__dirname, 'files'), { recursive: true });
    
        const output = fs.createWriteStream(
          path.join(
            __dirname,
            'files',
            `${randomBytes(6).toString('hex')}_${filename}`
          )
        );
    
        stream.pipe(output);
    
        await new Promise(function (resolve, reject) {
          output.on('close', () => {
            console.log('File uploaded');
            resolve();
          });
    
          output.on('error', (err) => {
            console.log(err);
            reject(err);
          });
        });
    
        return { filename, mimetype, encoding };
      },
    },
    }
    

Part 2 - React Frontend

  • Create react project with CRA template
npx create-react-app client
  • Install necessary dependencies
yarn add @apollo/client graphql apollo-upload-client

First we have to setup client for apollo react in order to make GraphQL requests to the backend server which sits http://localhost:5000/graphql. We will createUploadLink from apollo-upload-client instead of httpLink from @apollo/client.

//client/src/index.js

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

const uploadLink = createUploadLink({
  uri: 'http://localhost:5000/graphql',
});

const client = new ApolloClient({
  link: uploadLink,
  cache: new InMemoryCache(),
});

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
);

We can query for uploaded files using getPhotos query and render the link for files into the DOM. We can make use of useQuery hook for executing graphql query.

// client/src/components/Files.jsx

import { useQuery } from '@apollo/client';
import gql from 'graphql-tag';

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

function Files() {
  const { data, loading, error } = useQuery(GET_PHOTOS_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?.getPhotos.length && <p>No files uploaded yet</p>}
    </div>
  );
}

export default Files;

Since we have already setup apollo client with upload link. We can directly send file that user selects as variable singleUpload query.

// client/src/components/UploadFile.jsx

import gql from 'graphql-tag';
import { 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 [msg, setMsg] = useState('');
  const [uploadRequest, { loading, error }] = useMutation(
    SINGLE_UPLOAD_MUTATION
  );

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

  return (
    <div>
      <h3>{msg}</h3>
      <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;