React GraphQL File Upload with Apollo GraphQL

Aseer KT is a seasoned Full Stack Developer specializing in React, Node.js, and Go. Passionate about creating scalable web applications, he has a deep understanding of modern web technologies. Aseer is also an avid writer, contributing articles on web development and emerging tech, and actively participates in open source projects. Constantly exploring new tools and methodologies, he remains at the forefront of the industry.
File uploads are a crucial aspect of modern web applications, enabling users to share and manage various types of files, such as images, documents, and media. This functionality allows users to interact with applications in more dynamic and versatile ways, whether it's uploading a profile picture, sharing documents with colleagues, or attaching files to messages. Implementing file uploads involves handling the process of transferring files from the client to the server, processing them appropriately, and storing them securely. Techniques such as multipart form-data or GraphQL mutations with file scalars are commonly used to facilitate file uploads in web development. Overall, file uploads enhance the interactivity and utility of web applications, enriching user experiences and expanding the capabilities of online platforms.
You can see the final version of the working React GraphQL upload example here.
Part 1 - Setting Up a File Upload Server with Apollo and Express
In this article, we will guide you through setting up a file upload server using Apollo Server, Express, and GraphQL. This server will handle file uploads, store them locally, and provide a query to retrieve the list of uploaded files.
Project Structure
Before diving into the code, let's outline the structure of our project:
project-root/
├── server/
│ ├── index.js
│ ├── typeDefs.js
│ └── resolvers.js
└── files/
The files directory will be used to store uploaded files. Now, let's walk through each part of the implementation.
Server Setup
First, we set up the main server in server/index.js:
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import cors from "cors";
import express from "express";
import graphqlUploadExpress from "graphql-upload/graphqlUploadExpress.mjs";
import { resolvers } from "./resolvers.js";
import { typeDefs } from "./typeDefs.js";
const app = express();
// This middleware should be added before calling `applyMiddleware`.
app.use(graphqlUploadExpress());
app.use("/files", express.static("files"));
const server = new ApolloServer({
typeDefs,
resolvers,
});
await server.start();
app.use("/graphql", express.json(), cors(), expressMiddleware(server));
await new Promise((r) => app.listen({ port: 5000 }, r));
console.log(`🚀 Server ready at http://localhost:5000/graphql`);
In this code:
We import necessary modules and middlewares.
We configure
graphqlUploadExpressmiddleware to handle file uploads.We serve static files from the
filesdirectory.We create an instance of
ApolloServerwith our type definitions and resolvers.We start the server and set it to listen on port 5000.
Type Definitions
Next, we define our GraphQL schema in server/typeDefs.js:
export const typeDefs = `#graphql
scalar Upload
type File {
filename: String!
mimetype: String!
encoding: String!
}
type Query {
getFiles: [String!]
}
type Mutation {
singleUpload(file: Upload!): File!
}
`;
Here:
We define a custom scalar
Uploadto handle file uploads.We define a
Filetype to represent uploaded files.We provide a
getFilesquery to retrieve the list of uploaded files.We provide a
singleUploadmutation to handle single file uploads.
Resolvers
Finally, we implement the resolvers in server/resolvers.js:
import { randomBytes } from "crypto";
import {
createWriteStream,
existsSync,
mkdirSync,
readdirSync,
unlink,
} from "fs";
import GraphQLUpload from "graphql-upload/GraphQLUpload.mjs";
import path from "path";
export const resolvers = {
Upload: GraphQLUpload,
Query: {
getFiles: async function () {
if (existsSync("files")) {
const files = readdirSync("files");
return files.map((n) => `http://localhost:5000/files/${n}`);
} else return [];
},
},
Mutation: {
singleUpload: async function (_root, { file }) {
const { createReadStream, filename, encoding, mimetype } = await file;
const stream = createReadStream();
const uploadPath = "files";
const uploadFileName = `${randomBytes(6).toString("hex")}_${filename}`;
const uploadFileUrl = path.join(uploadPath, uploadFileName);
mkdirSync(uploadPath, { recursive: true });
const output = createWriteStream(uploadFileUrl);
stream.pipe(output);
stream.on("error", (error) => output.destroy(error));
await new Promise(function (resolve, reject) {
output.on("close", () => {
console.log("File uploaded");
resolve();
});
output.on("error", (err) => {
console.log(err);
unlink(uploadFileUrl, () => {
reject(err);
});
});
});
return { filename, mimetype, encoding };
},
},
};
In the resolvers:
The
Uploadscalar is handled byGraphQLUpload.The
getFilesquery checks if thefilesdirectory exists, reads its contents, and returns the URLs of the stored files.The
singleUploadmutation handles file uploads by reading the file stream, generating a unique file name, saving the file to thefilesdirectory, and returning the file details.
Running the Server
To run the server, execute the following commands in your project root:
pnpm install
node server/index.js
You should see the message 🚀 Server ready at http://localhost:5000/graphql. You can now use this URL to interact with your GraphQL server.
Part 1 Conclusion
In this section, we set up a file upload server using Apollo Server and Express. We defined the necessary GraphQL schema and resolvers to handle file uploads and retrievals. This setup provides a robust foundation for building applications that require file upload capabilities.
Part 2 - Building a File Upload Client with React and Apollo Client
In this article, we will guide you through setting up a client-side application using React and Apollo Client to handle file uploads. This client will interact with the file upload server we previously set up and provide an interface for uploading and retrieving files.
Project Structure
Before diving into the code, let's outline the structure of our client project:
project-root/
├── client/
│ ├── src/
│ │ ├── components/
│ │ │ ├── Files.jsx
│ │ │ └── UploadFile.jsx
│ │ ├── App.jsx
│ │ └── index.jsx
│ └── index.html
Setting Up Apollo Client
First, we set up Apollo Client in client/src/index.jsx:
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
// `uploadLink` is a terminating link
const uploadLink = createUploadLink({
uri: "http://localhost:5000/graphql",
headers: {
"Apollo-Require-Preflight": "true",
},
});
// `uploadLink` is used instead of httpLink
const client = new ApolloClient({
link: uploadLink,
cache: new InMemoryCache(),
});
const root = createRoot(document.getElementById("root"));
// client instance is provided to the entire react app using ApolloProvider
// Now we can use apollo hooks (useQuery, useMutation, ...) in child components
root.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
In this code:
We import necessary modules from Apollo Client and React.
We configure
createUploadLinkto handle file uploads by setting the GraphQL endpoint.We create an instance of
ApolloClientwith the upload link and an in-memory cache.We use
ApolloProviderto make the Apollo Client available throughout the React app.
Uploading Files
Next, we implement the UploadFile component in client/src/components/UploadFile.jsx:
import { gql, 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 [uploadRequest, { loading, error }] = useMutation(
SINGLE_UPLOAD_MUTATION
);
const uploadFile = async () => {
if (!file) return;
try {
const res = await uploadRequest({
variables: { file },
refetchQueries: [{ query: GET_FILES_QUERY }],
});
if (res.data) {
setFile(null);
}
} catch (err) {
console.error(err);
}
};
return (
<div>
<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;
In this component:
We define the
SINGLE_UPLOAD_MUTATIONto handle file uploads.We use
useStateto manage the selected file.We use
useMutationto execute the upload mutation.We provide a file input and a button to trigger the upload process, displaying any loading or error messages.
Displaying Uploaded Files
Finally, we implement the Files component in client/src/components/Files.jsx:
import { gql, useQuery } from '@apollo/client';
export const GET_FILES_QUERY = gql`
query GetFiles {
getFiles
}
`;
function Files() {
const { data, loading, error } = useQuery(GET_FILES_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?.getFiles.length && <p>No files uploaded yet</p>}
</div>
);
}
export default Files;
In this component:
We define the
GET_FILES_QUERYto fetch the list of uploaded files.We use
useQueryto execute the query and manage loading and error states.We display the list of files as links, handling cases where no files are uploaded yet.
Integrating Components
We integrate these components in our main app file, client/src/App.jsx:
import React from "react";
import UploadFile from "./components/UploadFile";
import Files from "./components/Files";
function App() {
return (
<div className="App">
<h1>File Upload with Apollo Client</h1>
<UploadFile />
<Files />
</div>
);
}
export default App;
In this code:
We import
UploadFileandFilescomponents.We render these components within a simple layout.
Running the Client
To run the client, execute the following commands in your project root:
pnpm install
pnpm dev
Your React app should now be running on http://localhost:3000, providing an interface to upload files and view the list of uploaded files.
Part 2 Conclusion
In this section, we set up a client-side application using React and Apollo Client to handle file uploads. We defined components to upload files and display the list of uploaded files. This setup complements the server-side implementation and provides a complete solution for file upload functionality.


