Implementation of a GraphQL mutation with file upload

I'm going to explain in this article how to implement a form with a file input that will upload the file to the server using a GraphQL mutation.

UPDATE March 2020: article and repo updated for Symfony 5 and React 16.8+ with hooks

Implementing a GraphQL API mutation that can upload files is not a very well documented task.

I'm going to explain in this article how to implement a form with a file input that will upload the file to the server using a GraphQL mutation. We will make use Symfony's OverblogGraphQLBundle to implement the GraphQL API and React Apollo on the client side.

We are going to implement a use case where a user can update their profile (name and picture). The complete source code of the example implemented in this article is available on Github.

Implementation of the GraphQL Mutation with Symfony

Install the OverblogGraphQLBundle first.

Don't forget to enable mutations in the bundle configuration:

# config/packages/graphql.yaml
overblog_graphql:
    definitions:
        schema:
            query: Query
            mutation: Mutation # Don't forget this line
        # ...

Configure GraphQL types

Let's create our mutation that will take 2 fields:

  • A name field (text).
  • A picture field (the file upload).
# graphql-server/config/graphql/types/Mutation.type.yaml
Mutation:
    type: object
    config:
        fields:
            UpdateUserProfile:
                type: UpdateUserProfilePayload
                resolve: "@=mutation('updateUserProfile', [args['input']['name'], args['input']['picture']])"
                args:
                    input: 'UpdateUserProfileInput!'

Define a scalar to handle file uploads:

# graphql-server/config/graphql/types/FileUpload.yaml
FileUpload:
    type: custom-scalar
    config:
        scalarType: '@=newObject("Overblog\\GraphQLBundle\\Upload\\Type\\GraphQLUploadType")'

We can now make use of this scalar for the UpdateUserProfileInput type:

# graphql-server/config/graphql/types/UpdateUserProfileInput.type.yaml
UpdateUserProfileInput:
    type: relay-mutation-input
    config:
        fields:
            name:
                type: 'String!'
           picture:
                type: 'FileUpload'

Let's define an output for our mutation (a totally random one, for the example):

# config/graphql/types/UpdateUserProfilePayload.type.yaml
UpdateUserProfilePayload:
    type: relay-mutation-payload
    config:
        fields:
            # Add all needed fields for the payload depending on your business logic
            name:
                type: 'String!'
            filename:
                type: 'String'

Implement the mutation resolver

# src/GraphQL/Mutation/UserProfileMutation.php
<?php

namespace App\GraphQL\Mutation;

use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface;
use Overblog\GraphQLBundle\Definition\Resolver\MutationInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;

class UserProfileMutation implements MutationInterface, AliasedInterface
{
    public function update(string $name, UploadedFile $pictureFile): array
    {
        // Add your business logic for the $name and $pictureFile variables
        // Eg. Persist user in database and upload the file to AWS S3
        // The important thing to see here it that we have our uploaded file!

        // This matches what we defined in UpdateUserProfilePayload.type.yaml
        // but this is just some "random" data for the example here
        return [
            'name'     => $name,
            'filename' => $pictureFile->getClientOriginalName(),
        ];
    }

    /**
     * {@inheritdoc}
     */
    public static function getAliases(): array
    {
        return [
            'update' => 'updateUserProfile',
        ];
    }
}

Consume the GraphQL Mutation with React Apollo

Install and configure React Apollo

Install Apollo for your React app:

yarn add apollo-boost react-apollo graphql

Install additional dependencies for Apollo to make uploads work:

yarn add apollo-upload-client

Create and configure the Apollo Client to be able to upload files, using createUploadLink:

// src/constants/apolloClient.js
import { ApolloClient } from 'apollo-boost';
import { createUploadLink } from 'apollo-upload-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const APOLLO_CLIENT = new ApolloClient({
  cache: new InMemoryCache(),
  link: createUploadLink({
    // Change URL of your Symfony GraphQL endpoint if needed
    // or use an environment variable, which is better
    uri: `http//localhost/graphql/`,
  }),
});

export default APOLLO_CLIENT;

Update your main file to make use of React Apollo and make use of the component we are going to implement next:

import React from 'react';
import UpdateProfilePictureForm from './UpdateProfilePictureForm';
import APOLLO_CLIENT from './constants/apolloClient';
import { ApolloProvider } from 'react-apollo';

const App = () => (
  <ApolloProvider client={APOLLO_CLIENT}>
    <div className="App">
      <UpdateProfilePictureForm />
    </div>
  </ApolloProvider>
);

export default App;

Create form

Let's create our form with a file input. The file will be sent along a name text field using a GraphQL mutation. The file will automatically be handled thanks to the way we created the Apollo Client in the previous step.

import React, { useState } from 'react';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';

const UPDATE_PROFILE = gql`
  mutation UpdateUserProfile($profile: UpdateUserProfileInput!) {
    UpdateUserProfile(input: $profile) {
      name
      filename
    }
  }
`;

const UpdateProfilePictureForm = () => {
  const [name, setName] = useState('');
  const [picture, setPicture] = useState(null);
  const [responseName, setResponseName] = useState('');
  const [responseFileName, setResponseFileName] = useState('');

  const handleNameChange = e => {
    setName(e.target.value);
  }

  const handlePictureChange = e => {
    setPicture(e.target.files[0]);
  }

  const handleSubmit = (e, updateUserProfile) => {
    e.preventDefault();

    const profile = {
      name,
      picture,
    };

    updateUserProfile({ variables: { profile } }).then(({ data: { UpdateUserProfile } }) => {
      setName('');
      setPicture(null);
      setResponseName(UpdateUserProfile.name);
      setResponseFileName(UpdateUserProfile.filename);
    });
  };

  return (
    <div>
      <Mutation mutation={UPDATE_PROFILE}>
        {(updateUserProfile) => (
          <div>
            <form onSubmit={e => handleSubmit(e, updateUserProfile)} >
              <div className="form-group">
                <label htmlFor="name">Your name</label>
                <input
                  type="text"
                  id="name"
                  name="name"
                  className="form-control"
                  value={name}
                  onChange={handleNameChange}
                />
              </div>

              <div className="form-group">
                <label htmlFor="pictureFile">Choose picture</label>
                <input
                  type="file"
                  id="pictureFile"
                  name="pictureFile"
                  className="form-control"
                  onChange={handlePictureChange}
                />
              </div>

              <button type="submit" className="btn btn-primary">Update profile</button>
            </form>

            <div>
              Server response:

              <ul>
                <li>Name: {responseName}</li>
                <li>Filename: {responseFileName}</li>
              </ul>
            </div>
          </div>
        )}
      </Mutation>
    </div>
  );
};

export default UpdateProfilePictureForm;

You are now all set with your GraphQL mutation and its interface to use it!