Handling File Uploads

1. Understanding Upload Scalar

AspectDetail
Specgraphql-multipart-request-spec
Scalarscalar 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

CheckDetail
MIME allowlistimage/jpeg, image/png, application/pdf
Magic bytesVerify actual content (file-type lib)
ExtensionDon'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

StorageProsCons
S3 / R2 / GCSDurable, scalable, CDN-readyCosts egress
Local diskSimpleNot horizontally scalable
Database BLOBTransactionalSlow, bloats DB

8. Generating File URLs

TypeUse
Public CDN URLStatic assets
Pre-signedTemporary, scoped access
Proxy URLServer-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

ApproachDetail
Client onProgressXHR upload progress events
Tus protocolResumable uploads
Direct-to-S3Pre-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.