Apollo 20: File Uploads
easy⏱ 5 mincourseapollo
How file uploads work in GraphQL
Standard GraphQL operations send JSON over HTTP. File uploads require multipart form data (like traditional HTML file uploads). The apollo-upload-client package provides createUploadLink, which detects File or Blob objects in your mutation variables and automatically switches to multipart encoding. The server expects an Upload scalar type. The flow:
- Client sends a multipart request with the file and GraphQL operation.
- Server parses the multipart body and provides a readable stream.
- Server resolves the
Upload scalar to get the file stream.
- Your resolver reads the stream and saves the file.
import createUploadLink from 'apollo-upload-client/createUploadLink.mjs';
const uploadLink = createUploadLink({
uri: '/graphql',
headers: {
'Apollo-Require-Preflight': 'true',
},
});
const client = new ApolloClient({
link: uploadLink,
cache: new InMemoryCache(),
});
Build upload mutation
Define an UPLOAD_FILE mutation that accepts $file: Upload! and returns id, filename, mimetype, and url. Create a file input component that captures the selected File object. Call the mutation with variables: { file } — the upload link handles the rest. Display upload status and the returned file URL on success. Add onProgress tracking via the context option in the mutation call.
Progress tracking
To track upload progress, use the context option with fetchOptions to pass an onUploadProgress callback. However, createUploadLink doesn't natively support progress tracking. For progress, use XMLHttpRequest or the Fetch API's ReadableStream wrapped in a custom link. A simpler approach: show an indeterminate progress bar during upload and rely on the mutation's loading state. For large files, consider pre-signed URLs (upload directly to S3/GCS, then send the URL to your API).
// Alternative: pre-signed URL pattern
const GENERATE_UPLOAD_URL = gql`
mutation GenerateUploadUrl($filename: String!, $contentType: String!) {
generateUploadUrl(filename: $filename, contentType: $contentType) {
uploadUrl
fileUrl
}
}
`;
async function uploadWithPresignedUrl(file) {
const { data } = await generateUrl({
variables: { filename: file.name, contentType: file.type },
});
await fetch(data.generateUploadUrl.uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
});
return data.generateUploadUrl.fileUrl;
}
Upload link vs HTTP link
createUploadLink is a drop-in replacement for HttpLink. It handles regular JSON requests identically but also detects File/Blob objects in variables and switches to multipart encoding. You don't need both — just replace new HttpLink(...) with createUploadLink(...). If you use other links in your chain (auth, error, retry), place the upload link as the terminating link (last in the chain), just like you would with HttpLink.