GraphQL File Upload using React with Apollo GraphQL
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
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,
- Backend GraphQL Server
- Frontend React
Folder Structure
- client directory is where our react client sits.
- server directory is where the backend graphql server sits.
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 projectyarn 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
- Theupload 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
- TheMIME 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;