File Uploads in GraphQL
File uploads are a common need but GraphQL doesn't have built-in support for them. The standard approach is to use a separate upload endpoint or the multipart request spec.
Multipart Request Specification
The GraphQL multipart request spec lets you send files alongside your GraphQL operations using multipart/form-data:
// Schema with file upload
const typeDefs = `
scalar Upload
type File {
id: ID!
filename: String!
mimetype: String!
encoding: String!
url: String!
}
type Mutation {
uploadFile(file: Upload!): File!
}
`;
The Upload scalar is provided by libraries like graphql-upload.
Server Implementation
Here's how to handle file uploads on the server with graphql-upload:
const { GraphQLUpload, graphqlUploadExpress } = require('graphql-upload');
const { createWriteStream } = require('fs');
const path = require('path');
const resolvers = {
Upload: GraphQLUpload,
Mutation: {
uploadFile: async (parent, { file }) => {
const { createReadStream, filename, mimetype, encoding } = await file;
// Create a unique filename
const uniqueFilename = `${Date.now()}-${filename}`;
const uploadPath = path.join(__dirname, 'uploads', uniqueFilename);
// Save the file
await new Promise((resolve, reject) => {
createReadStream()
.pipe(createWriteStream(uploadPath))
.on('finish', resolve)
.on('error', reject);
});
// Return file metadata
return {
id: uniqueFilename,
filename,
mimetype,
encoding,
url: `/uploads/${uniqueFilename}`,
};
},
},
};
// Add middleware to Express
app.use(graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }));
The graphql-upload middleware parses multipart requests and makes files available in resolvers.
Client-Side Upload
On the client, use the useMutation hook with a special upload function:
import { useMutation, gql } from '@apollo/client';
const UPLOAD_FILE = gql`
mutation UploadFile($file: Upload!) {
uploadFile(file: $file) {
id
filename
url
}
}
`;
function FileUpload() {
const [uploadFile] = useMutation(UPLOAD_FILE);
const handleFileChange = async (e) => {
const file = e.target.files[0];
if (!file) return;
await uploadFile({
variables: { file },
});
};
return (
<input
type="file"
onChange={handleFileChange}
/>
);
}
Apollo Client automatically handles the multipart encoding when you pass a File object.
Alternative: Separate Upload Endpoint
Some teams prefer a separate REST endpoint for file uploads, then pass the file URL to GraphQL:
// REST endpoint for upload
app.post('/api/upload', upload.single('file'), (req, res) => {
res.json({ url: `/uploads/${req.file.filename}` });
});
// Then use the URL in GraphQL
const UPLOAD_AVATAR = gql`
mutation UploadAvatar($url: String!) {
updateMe(input: { avatarUrl: $url }) {
id
avatarUrl
}
}
`;
// Client code
const uploadFile = async (file) => {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
const { url } = await response.json();
// Now use the URL in GraphQL
await uploadAvatar({ variables: { url } });
};
This approach is simpler and leverages existing upload libraries.