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
graphqlUploadExpress
middleware to handle file uploads.We serve static files from the
files
directory.We create an instance of
ApolloServer
with 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
Upload
to handle file uploads.We define a
File
type to represent uploaded files.We provide a
getFiles
query to retrieve the list of uploaded files.We provide a
singleUpload
mutation 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
Upload
scalar is handled byGraphQLUpload
.The
getFiles
query checks if thefiles
directory exists, reads its contents, and returns the URLs of the stored files.The
singleUpload
mutation handles file uploads by reading the file stream, generating a unique file name, saving the file to thefiles
directory, 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
createUploadLink
to handle file uploads by setting the GraphQL endpoint.We create an instance of
ApolloClient
with the upload link and an in-memory cache.We use
ApolloProvider
to 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_MUTATION
to handle file uploads.We use
useState
to manage the selected file.We use
useMutation
to 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_QUERY
to fetch the list of uploaded files.We use
useQuery
to 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
UploadFile
andFiles
components.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.