Learn GraphQL - File Upload with GraphQL Upload
Episode 17 of 51

Learn GraphQL - File Upload with GraphQL Upload

Episode 17 handles file uploads in GraphQL: the GraphQL Upload specification with multipart requests, implementing graphql-upload in Apollo Server, stream processing with type and size validation, AWS S3 and Cloudinary integration, and file upload security.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Files aren't ordinary data — they're large, have special formats, and need streaming handling so they don't burden the server's memory. Episode 17 covers file upload in GraphQL with the modern GraphQL Upload specification.

We'll compare upload approaches, implement graphql-upload in Apollo Server, process files with type and size validation, integrate storage like AWS S3 and Cloudinary, and discuss file upload security.

File Upload Approaches

GraphQL Upload and Multipart

The most practical approach is the GraphQL Upload specification: the file is sent as multipart/form-data together with the GraphQL operation in a single request. The server exposes an Upload scalar, and the client sends the file as a variable of type Upload.

Other approaches you should know:

  • Base64 encoding: converting the file into a base64 string inside JSON — strongly discouraged, the payload grows by a third and the whole file is loaded into memory.
  • Pre-signed URLs: the client uploads directly to object storage (S3, GCS), and GraphQL only receives a URL reference. This is most efficient for large files and is recommended for large-scale production.

Implementing graphql-upload

Installation and the Upload Scalar

Install the graphql-upload package with npm install graphql-upload:

Install graphql-upload
npm install graphql-upload

In the schema, add the scalar and use it on mutations:

Schema with Upload
scalar Upload
 
type Mutation {
  uploadAvatar(file: Upload!): User!
  uploadPostImages(files: [Upload!]!): Post!
}

Integration with Apollo Server

Apollo Server 4 no longer ships a built-in upload middleware, so mount graphqlUploadExpress manually on the HTTP server:

JSUpload middleware in Express
import { graphqlUploadExpress } from "graphql-upload/express.js";
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import express from "express";
 
const app = express();
app.use("/graphql", graphqlUploadExpress({ maxFileSize: 5 * 1024 * 1024, maxFiles: 5 }));
app.use("/graphql", expressMiddleware(server));
 
app.listen(4000);

The maxFileSize and maxFiles parameters are the first line of defense against abuse.

File Processing

Stream Handling and Validation

A file from graphql-upload is a stream (a FileUpload object). Don't read an entire large file into memory — process it in parts or stream it straight to storage:

JSUpload resolver with validation
import { Readable } from "stream";
 
async function uploadAvatar(_, args, ctx) {
  const { createReadStream, mimetype, filename } = await args.file;
 
  const allowed = ["image/jpeg", "image/png", "image/webp"];
  if (!allowed.includes(mimetype)) {
    throw new Error("Tipe file tidak diizinkan");
  }
  if (filename.length > 100) {
    throw new Error("Nama file terlalu panjang");
  }
 
  const stream = createReadStream();
  const key = await ctx.storage.uploadStream(stream, { contentType: mimetype });
 
  return ctx.userService.updateAvatar(ctx.user.id, key);
}

mimetype and size validation happen before the file is processed. For images, process them first with sharp — for example, resizing an avatar to 128x128 and converting the format to webp — so the stored file is controlled:

Install sharp
npm install sharp
JSProcess an image with sharp
import sharp from "sharp";
 
const buffer = await sharp(await streamToBuffer(stream))
  .resize(128, 128)
  .webp({ quality: 80 })
  .toBuffer();

Remember: never trust the mimetype the client sends — detect the actual type from the file content, for example with file-type, because mimetype can be forged.

Storage Solutions

AWS S3 and Cloudinary

Writing files to a local filesystem is only suitable for development — local storage isn't distributed and disappears when the instance is recreated. For production, use object storage:

Install the AWS SDK
npm install @aws-sdk/client-s3
JSUpload to S3
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
 
const s3 = new S3Client({ region: process.env.AWS_REGION });
 
async function uploadToS3(key, body) {
  await s3.send(new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: key,
    Body: body,
  }));
  return `https://cdn.kalian.com/${key}`;
}

Popular alternatives: Cloudinary for images with on-the-fly transformations, and Google Cloud Storage as the GCP equivalent. Database BLOB storage isn't recommended — large files clog the database and are hard to back up.

File Upload Security

Security Checklist

File upload is a very popular attack vector. Apply the following checklist:

  • Type validation: detect the type from content, not just mimetype.
  • Size limits: maxFileSize in the middleware plus additional validation in the resolver.
  • Rate limiting: limit the number of uploads per user (episode 15).
  • Malicious file detection: scan publicly uploaded files with antivirus.
  • Safe filenames: never use the original filename directly; generate a random key.
  • Block execution: serve files from a separate domain or CDN, not from the application server.
JSA safe random filename
import crypto from "crypto";
 
const key = `${Date.now()}-${crypto.randomUUID()}.webp`;

Conclusion

Key takeaways:

  • Use the GraphQL Upload specification with multipart for files, or pre-signed URLs for large files.
  • Add the Upload scalar to the schema and mount the graphqlUploadExpress middleware manually in Apollo 4.
  • Process files as streams; process images with sharp before storing.
  • Validate types from file content, not from the forgeable mimetype.
  • Store files in object storage like S3, not on the local filesystem or database BLOBs.
  • Apply size limits, rate limiting, antivirus scanning, and random filenames.

In the next episode, episode 18, you'll learn about advanced schema design patterns — schema design principles, designing one-to-one through many-to-many relations, global object identification with the Node interface, mutation payload design, API evolution strategies with deprecation, and schema modularization. Your schema will be designed like an experienced architect's!

Learn GraphQL - File Upload with GraphQL Upload | Learn GraphQL