Episode 17 menangani upload file di GraphQL: spesifikasi GraphQL Upload dengan multipart request, implementasi graphql-upload di Apollo Server, pemrosesan stream dengan validasi tipe dan ukuran, integrasi AWS S3 dan Cloudinary, hingga keamanan file upload.

File bukan data biasa — ukurannya besar, berformat khusus, dan membutuhkan penanganan streaming agar tidak membebani memori server. Episode 17 membahas upload file di GraphQL dengan spesifikasi GraphQL Upload yang modern.
Kita akan membandingkan pendekatan upload, mengimplementasikan graphql-upload di Apollo Server, memproses file dengan validasi tipe dan ukuran, mengintegrasikan penyimpanan seperti AWS S3 dan Cloudinary, serta membahas keamanan upload file.
Cara paling praktis adalah GraphQL Upload specification: file dikirim sebagai multipart/form-data bersama dengan operasi GraphQL dalam satu request. Server mengekspos scalar Upload, dan client mengirim file sebagai variabel yang tipe-nya Upload.
Pendekatan lain yang perlu kalian kenali:
Pasang package graphql-upload dengan npm install graphql-upload:
npm install graphql-uploadDi schema, tambahkan scalar dan gunakan pada mutation:
scalar Upload
type Mutation {
uploadAvatar(file: Upload!): User!
uploadPostImages(files: [Upload!]!): Post!
}Apollo Server 4 tidak lagi menyertakan middleware upload bawaan, jadi pasang graphqlUploadExpress secara manual saat server HTTP:
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);Parameter maxFileSize dan maxFiles adalah garis pertahanan pertama terhadap penyalahgunaan.
File dari graphql-upload adalah stream (berupa objek FileUpload). Jangan baca seluruh file ke memori untuk file besar — proses per bagian atau stream langsung ke penyimpanan:
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);
}Validasi mimetype dan ukuran dilakukan sebelum file diproses. Untuk gambar, olah dulu dengan sharp — misalnya meresize avatar menjadi 128x128 dan mengubah format ke webp — sehingga file yang disimpan terkontrol:
npm install sharpimport sharp from "sharp";
const buffer = await sharp(await streamToBuffer(stream))
.resize(128, 128)
.webp({ quality: 80 })
.toBuffer();Ingat: jangan pernah percaya nilai mimetype yang dikirim client — deteksi tipe sebenarnya dari isi file, misalnya dengan file-type, karena mimetype bisa dipalsukan.
Menulis file ke filesystem lokal hanya cocok untuk development — storage lokal tidak terdistribusi dan hilang saat instance di-recreate. Untuk production gunakan object storage:
npm install @aws-sdk/client-s3import { 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}`;
}Alternatif yang populer: Cloudinary untuk gambar dengan transformasi on-the-fly, dan Google Cloud Storage sebagai padanan GCP. Database BLOB storage tidak direkomendasikan — file besar menyumbat database dan sulit di-backup.
Upload file adalah vektor serangan yang sangat populer. Terapkan checklist berikut:
mimetype.maxFileSize di middleware dan validasi tambahan di resolver.import crypto from "crypto";
const key = `${Date.now()}-${crypto.randomUUID()}.webp`;Inti yang harus dibawa pulang:
Upload ditambahkan ke schema dan middleware graphqlUploadExpress dipasang manual di Apollo 4.mimetype yang bisa dipalsukan.Di episode 18 selanjutnya kalian akan mempelajari advanced schema design patterns — prinsip desain schema, desain relasi one-to-one sampai many-to-many, global object identification dengan Node interface, mutation payload design, strategi evolusi API dengan deprecation, hingga modularisasi schema. Schema kalian akan dirancang layaknya arsitek berpengalaman!