Implementation of a GraphQL mutation with file upload
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!