Labs ICT
Pro Login

File Uploads in GraphQL

File Uploads in GraphQL

File uploads are a common need but GraphQL doesn't have built-in support for them. The standard approach is to use a separate upload endpoint or the multipart request spec.

Multipart Request Specification

The GraphQL multipart request spec lets you send files alongside your GraphQL operations using multipart/form-data:

// Schema with file upload
const typeDefs = `
  scalar Upload
  
  type File {
    id: ID!
    filename: String!
    mimetype: String!
    encoding: String!
    url: String!
  }
  
  type Mutation {
    uploadFile(file: Upload!): File!
  }
`;

The Upload scalar is provided by libraries like graphql-upload.

Server Implementation

Here's how to handle file uploads on the server with graphql-upload:

const { GraphQLUpload, graphqlUploadExpress } = require('graphql-upload');
const { createWriteStream } = require('fs');
const path = require('path');

const resolvers = {
  Upload: GraphQLUpload,
  
  Mutation: {
    uploadFile: async (parent, { file }) => {
      const { createReadStream, filename, mimetype, encoding } = await file;
      
      // Create a unique filename
      const uniqueFilename = `${Date.now()}-${filename}`;
      const uploadPath = path.join(__dirname, 'uploads', uniqueFilename);
      
      // Save the file
      await new Promise((resolve, reject) => {
        createReadStream()
          .pipe(createWriteStream(uploadPath))
          .on('finish', resolve)
          .on('error', reject);
      });
      
      // Return file metadata
      return {
        id: uniqueFilename,
        filename,
        mimetype,
        encoding,
        url: `/uploads/${uniqueFilename}`,
      };
    },
  },
};

// Add middleware to Express
app.use(graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }));

The graphql-upload middleware parses multipart requests and makes files available in resolvers.

Client-Side Upload

On the client, use the useMutation hook with a special upload function:

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

const UPLOAD_FILE = gql`
  mutation UploadFile($file: Upload!) {
    uploadFile(file: $file) {
      id
      filename
      url
    }
  }
`;

function FileUpload() {
  const [uploadFile] = useMutation(UPLOAD_FILE);

  const handleFileChange = async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    await uploadFile({
      variables: { file },
    });
  };

  return (
    <input
      type="file"
      onChange={handleFileChange}
    />
  );
}

Apollo Client automatically handles the multipart encoding when you pass a File object.

Alternative: Separate Upload Endpoint

Some teams prefer a separate REST endpoint for file uploads, then pass the file URL to GraphQL:

// REST endpoint for upload
app.post('/api/upload', upload.single('file'), (req, res) => {
  res.json({ url: `/uploads/${req.file.filename}` });
});

// Then use the URL in GraphQL
const UPLOAD_AVATAR = gql`
  mutation UploadAvatar($url: String!) {
    updateMe(input: { avatarUrl: $url }) {
      id
      avatarUrl
    }
  }
`;

// Client code
const uploadFile = async (file) => {
  const formData = new FormData();
  formData.append('file', file);
  
  const response = await fetch('/api/upload', {
    method: 'POST',
    body: formData,
  });
  
  const { url } = await response.json();
  
  // Now use the URL in GraphQL
  await uploadAvatar({ variables: { url } });
};

This approach is simpler and leverages existing upload libraries.