Handling File Uploads
1. Understanding Upload Scalar
| Aspect | Detail |
| Spec | graphql-multipart-request-spec |
| Scalar | scalar Upload |
| Resolver value | { filename, mimetype, encoding, createReadStream() } |
2. Configuring Upload Middleware
Example: graphql-upload setup
import { graphqlUploadExpress } from "graphql-upload-minimal";
import { GraphQLUpload } from "graphql-upload-minimal";
app.use(graphqlUploadExpress({ maxFileSize: 10_000_000, maxFiles: 10 }));
const resolvers = { Upload: GraphQLUpload, Mutation: { /* ... */ } };
3. Defining Upload Mutation
Example: Upload mutation SDL
scalar Upload
type Mutation {
uploadAvatar(file: Upload!): Image!
uploadFiles(files: [Upload!]!): [File!]!
}
4. Processing Uploaded Files
Example: Stream to disk
uploadAvatar: async (_, { file }) => {
const { createReadStream, filename, mimetype } = await file;
const path = `/uploads/${randomUUID()}-${filename}`;
await pipeline(createReadStream(), createWriteStream(path));
return { url: path, filename, mimetype };
}
5. Validating File Types
| Check | Detail |
| MIME allowlist | image/jpeg, image/png, application/pdf |
| Magic bytes | Verify actual content (file-type lib) |
| Extension | Don't trust client-supplied extension |
6. Validating File Size
Example: Size cap
let bytes = 0;
const MAX = 10 * 1024 * 1024;
stream.on("data", (chunk) => {
bytes += chunk.length;
if (bytes > MAX) stream.destroy(new Error("File too large"));
});
7. Storing Files
| Storage | Pros | Cons |
| S3 / R2 / GCS | Durable, scalable, CDN-ready | Costs egress |
| Local disk | Simple | Not horizontally scalable |
| Database BLOB | Transactional | Slow, bloats DB |
8. Generating File URLs
| Type | Use |
| Public CDN URL | Static assets |
| Pre-signed | Temporary, scoped access |
| Proxy URL | Server-controlled access (auth check on download) |
9. Handling Multiple Uploads
Example: Parallel processing
uploadFiles: async (_, { files }) => {
const resolved = await Promise.all(files);
return Promise.all(resolved.map(processFile));
}
10. Implementing Progress Tracking
| Approach | Detail |
| Client onProgress | XHR upload progress events |
| Tus protocol | Resumable uploads |
| Direct-to-S3 | Pre-signed URL bypasses GraphQL — best for big files |
Note: For files > 10 MB prefer pre-signed S3 PUT URLs returned from a GraphQL mutation, then notify the API on completion.