Pemrosesan Gambar dengan Claude Code: Sharp, Canvas, WebP/AVIF, dan Validasi Upload
Bangun image processing aman dengan Claude Code: Sharp, Canvas, EXIF, WebP/AVIF, validasi, job, dan test.
Pemrosesan gambar sering terlihat seperti fitur kecil: user mengunggah foto, aplikasi membuat thumbnail, halaman terasa lebih cepat. Di production, fitur ini menjadi batas teknis yang serius. Kita perlu validasi upload, nama file yang aman, penghapusan EXIF, resize, kompresi, fallback format WebP/AVIF, background job, privasi, dan test.
Claude Code berguna karena implementasi menyentuh banyak file sekaligus. Namun prompt yang terlalu umum seperti “optimalkan gambar” bisa menghasilkan kode yang percaya pada file.type, memakai nama file asli di URL publik, menyimpan metadata, mengubah semua gambar ke AVIF secara synchronous, dan tidak mengetes foto mobile yang rotasinya salah.
Untuk review, pakai sumber primer: dokumentasi Claude Code, Sharp resize API, Sharp output API, MDN File API, MDN Canvas toBlob, dan OWASP File Upload Cheat Sheet. Jika proses browser mulai berat, baca juga panduan internal Claude Code Web Worker.
Tentukan batas pemrosesan
Jangan mulai dari format. Mulai dari siapa yang bertanggung jawab atas bagian mana.
| Lokasi | Cocok untuk | Hindari |
|---|---|---|
| Browser | Preview, resize ringan, mengurangi ukuran upload | Validasi terpercaya, AVIF massal, keputusan privasi |
| Server request | MIME, magic bytes, dimensi, EXIF, thumbnail kecil | Banyak variant, AVIF lambat |
| Background job | Set gambar produk, regenerasi CMS, migrasi lama | Respons upload yang harus cepat |
Pola yang aman: browser memperbaiki UX, server selalu memvalidasi ulang, variant berat masuk ke job. accept="image/*" dan file.type hanya membantu UI, bukan batas keamanan.
flowchart LR
Browser["Browser preview / optional resize"]
Upload["Upload endpoint"]
Validate["Magic bytes, size, dimensions"]
Store["Private raw storage"]
Job["Background variants"]
Public["Public WebP/JPEG/AVIF"]
Browser --> Upload
Upload --> Validate
Validate --> Store
Store --> Job
Job --> Public
Saat meminta Claude Code, tulis aturan secara eksplisit: validasi MIME dan magic bytes di server, jangan pakai nama asli di URL publik, gunakan rotate(), jangan panggil .withMetadata() tanpa alasan jelas, dan buat AVIF opsional. Aturan kecil ini mengurangi banyak implementasi yang terlihat benar tetapi rapuh.
Use case produk
Use case pertama adalah ecommerce atau marketplace. Penjual biasanya mengunggah foto besar dari ponsel. Produk membutuhkan thumbnail persegi, gambar kartu, gambar detail, dan kadang gambar social sharing. Kompresi berlebihan merusak kepercayaan karena pembeli perlu melihat tekstur, warna, label, dan kondisi barang. Mulai dari WebP, lalu tambahkan AVIF setelah mengukur waktu encoding dan penghematan byte.
Use case kedua adalah avatar atau foto tim. Yang penting adalah crop persegi, URL aman, dan privasi. Nama seperti client-contract-final.png tidak boleh masuk path publik. Walaupun browser sudah mengecilkan file, output server tetap harus menghapus EXIF.
Use case ketiga adalah screenshot untuk blog, help center, atau kursus. Teks harus terbaca. Screenshot yang hemat 80 KB tetapi membuat label tombol buram adalah optimasi yang gagal. Jika gambar juga masuk ke alur dokumen, baca pembuatan PDF dengan Claude Code.
Use case keempat adalah lampiran privat SaaS: invoice, gambar verifikasi, screenshot support, atau dokumen admin. File seperti ini tidak boleh disimpan di public/uploads. Ia perlu private storage, access control, retention policy, dan audit log.
Setup
Contoh ini memakai Node.js 20 atau lebih baru. Modul yang sama bisa dipakai di Next.js, Express, Hono, Astro API routes, atau queue worker.
npm i sharp file-type p-limit
npm i -D tsx typescript @types/node
mkdir -p src public/uploads
Tambahkan command test agar bisa berjalan di CI.
{
"scripts": {
"test:images": "node --import tsx --test src/**/*.test.ts"
}
}
Validasi upload dan nama aman
Modul ini adalah batas kepercayaan server. Ia tidak percaya ekstensi, tetapi memeriksa magic bytes, membaca dimensi dengan Sharp, menolak file terlalu besar, dan memblokir gambar animasi atau multipage untuk flow ini.
// src/image-policy.ts
import { randomUUID } from "node:crypto";
import { fileTypeFromBuffer } from "file-type";
import sharp from "sharp";
const MAX_BYTES = 6 * 1024 * 1024;
const MAX_PIXELS = 24_000_000;
const EXTENSION_BY_MIME = {
"image/jpeg": ".jpg",
"image/png": ".png",
"image/webp": ".webp",
"image/avif": ".avif",
} as const;
export type MimeType = keyof typeof EXTENSION_BY_MIME;
export type ImageUploadInfo = {
mime: MimeType;
extension: string;
width: number;
height: number;
bytes: number;
originalName: string;
};
function isAllowedMime(mime: string): mime is MimeType {
return mime in EXTENSION_BY_MIME;
}
export async function assertImageUpload(
buffer: Buffer,
originalName = "upload",
): Promise<ImageUploadInfo> {
if (buffer.byteLength === 0) {
throw new Error("Empty file");
}
if (buffer.byteLength > MAX_BYTES) {
throw new Error("Image must be 6 MB or smaller");
}
const detected = await fileTypeFromBuffer(buffer);
if (!detected || !isAllowedMime(detected.mime)) {
throw new Error("Unsupported image type");
}
const metadata = await sharp(buffer, { failOn: "error" }).metadata();
if (!metadata.width || !metadata.height) {
throw new Error("Image dimensions could not be read");
}
if (metadata.pages && metadata.pages > 1) {
throw new Error("Animated images are not allowed here");
}
const pixels = metadata.width * metadata.height;
if (pixels > MAX_PIXELS) {
throw new Error("Image dimensions are too large");
}
return {
mime: detected.mime,
extension: EXTENSION_BY_MIME[detected.mime],
width: metadata.width,
height: metadata.height,
bytes: buffer.byteLength,
originalName,
};
}
export function safeImageName(mime: MimeType): string {
return `${randomUUID()}${EXTENSION_BY_MIME[mime]}`;
}
Nama asli boleh disimpan sebagai data tampilan privat, tetapi jangan menjadi URL publik. Nama acak juga mencegah benturan file dan masalah Unicode.
Resize, kompresi, dan EXIF dengan Sharp
Sharp adalah pilihan realistis untuk pemrosesan gambar di server Node.js. Detail pentingnya: pakai rotate() untuk menerapkan orientasi EXIF, lalu jangan simpan metadata kecuali ada kebutuhan jelas. Untuk gambar web publik, output tanpa metadata biasanya lebih aman.
// src/optimize-image.ts
import { mkdir } from "node:fs/promises";
import path from "node:path";
import sharp from "sharp";
type Variant = {
kind: "thumb" | "card" | "hero";
width: number;
height?: number;
};
const VARIANTS: Variant[] = [
{ kind: "thumb", width: 320, height: 320 },
{ kind: "card", width: 640 },
{ kind: "hero", width: 1280 },
];
export type OptimizedImage = {
src: string;
width: number;
height: number;
bytes: number;
format: "webp" | "avif";
};
export async function optimizeImage(
buffer: Buffer,
outputDir: string,
baseName: string,
makeAvif = false,
): Promise<OptimizedImage[]> {
await mkdir(outputDir, { recursive: true });
const results: OptimizedImage[] = [];
for (const variant of VARIANTS) {
const resized = sharp(buffer)
.rotate()
.resize({
width: variant.width,
height: variant.height,
fit: variant.height ? "cover" : "inside",
withoutEnlargement: true,
});
const webpName = `${baseName}-${variant.kind}.webp`;
const webpInfo = await resized
.clone()
.webp({ quality: 78, effort: 4 })
.toFile(path.join(outputDir, webpName));
results.push({
src: `/uploads/${webpName}`,
width: webpInfo.width,
height: webpInfo.height,
bytes: webpInfo.size,
format: "webp",
});
if (makeAvif) {
const avifName = `${baseName}-${variant.kind}.avif`;
const avifInfo = await resized
.clone()
.avif({ quality: 45, effort: 4 })
.toFile(path.join(outputDir, avifName));
results.push({
src: `/uploads/${avifName}`,
width: avifInfo.width,
height: avifInfo.height,
bytes: avifInfo.size,
format: "avif",
});
}
}
return results;
}
AVIF berguna, tetapi bukan aturan universal. Kompresinya bisa bagus, namun encoding sering lebih lambat. Untuk katalog besar, ukur gambar nyata dan tetap sediakan fallback WebP atau JPEG.
Endpoint upload Next.js
Ini contoh minimal App Router. Untuk gambar privat, ganti public/uploads dengan object storage dan akses terautentikasi.
// app/api/images/route.ts
import path from "node:path";
import { NextResponse } from "next/server";
import { assertImageUpload, safeImageName } from "@/src/image-policy";
import { optimizeImage } from "@/src/optimize-image";
export async function POST(request: Request) {
const form = await request.formData();
const file = form.get("image");
if (!(file instanceof File)) {
return NextResponse.json(
{ error: "image field is required" },
{ status: 400 },
);
}
const buffer = Buffer.from(await file.arrayBuffer());
const upload = await assertImageUpload(buffer, file.name);
const storedName = safeImageName(upload.mime);
const baseName = storedName.replace(/\.[^.]+$/, "");
const variants = await optimizeImage(
buffer,
path.join(process.cwd(), "public", "uploads"),
baseName,
false,
);
return NextResponse.json({
original: {
width: upload.width,
height: upload.height,
bytes: upload.bytes,
},
variants,
});
}
Contoh ini untuk media publik. Invoice, lampiran support, dan dokumen internal harus memakai flow privat.
Resize di browser
Resize browser mengurangi traffic dan mempercepat preview. Tetap saja, server harus memvalidasi ulang.
// src/resize-in-browser.ts
export async function resizeInBrowser(
file: File,
maxSide = 1600,
): Promise<File> {
const bitmap = await createImageBitmap(file);
const scale = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height));
const width = Math.round(bitmap.width * scale);
const height = Math.round(bitmap.height * scale);
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Canvas 2D context is not available");
}
context.drawImage(bitmap, 0, 0, width, height);
bitmap.close();
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob(
(result) => {
if (result) resolve(result);
else reject(new Error("Canvas export failed"));
},
"image/webp",
0.82,
);
});
const outputName = file.name.replace(/\.[^.]+$/, ".webp");
return new File([blob], outputName, {
type: blob.type || "image/webp",
lastModified: Date.now(),
});
}
Canvas re-encode sering menghapus banyak metadata, tetapi privasi tetap harus dijamin oleh output server. Export AVIF dari browser juga terlalu bergantung pada environment.
Background job dan budget performa
Request upload synchronous harus pendek: validasi, simpan, dan thumbnail minimum. Detail image, OGP, AVIF, dan regenerasi lama sebaiknya masuk job.
Budget awal yang realistis: avatar 320x320 di bawah 80 KB, card width 640 di bawah 120 KB, hero width 1280 di bawah 250 KB. Angka bisa berubah, tetapi budget mencegah konfigurasi Claude Code yang terlalu berat.
// src/batch-optimize.ts
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import pLimit from "p-limit";
import { assertImageUpload, safeImageName } from "./image-policy";
import { optimizeImage } from "./optimize-image";
export async function batchOptimize(inputDir: string, outputDir: string) {
const files = await readdir(inputDir);
const limit = pLimit(3);
const jobs = files.map((file) =>
limit(async () => {
const sourcePath = path.join(inputDir, file);
const buffer = await readFile(sourcePath);
const upload = await assertImageUpload(buffer, file);
const baseName = safeImageName(upload.mime).replace(/\.[^.]+$/, "");
const variants = await optimizeImage(buffer, outputDir, baseName, true);
return {
file,
variants: variants.length,
};
}),
);
return Promise.allSettled(jobs);
}
Jika memakai queue nyata, simpan job ID, source image ID, jenis variant, alasan gagal, retry count, dan path output. Tanpa itu, file bisa menjadi yatim tanpa catatan database.
Kegagalan umum
Kegagalan paling sering: percaya pada file.type, memakai nama asli di URL publik, mengabaikan orientasi EXIF, menyimpan metadata karena .withMetadata(), membuat AVIF di dalam request, atau mengompresi screenshot sampai teks tidak terbaca.
Ada juga kegagalan bisnis. Screenshot kursus yang buram mengurangi kepercayaan pada CTA produk berbayar. Gambar produk yang lambat membuat tombol beli muncul sebelum user memahami produk. Untuk mengukur image load, CTA click, dan purchase path bersama, lihat analytics dengan Claude Code.
Test perilaku
Buat gambar di dalam test agar CI tidak bergantung pada fixture manual.
// src/image-policy.test.ts
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import sharp from "sharp";
import { assertImageUpload, safeImageName } from "./image-policy";
import { optimizeImage } from "./optimize-image";
test("validates and optimizes a generated image", async () => {
const input = await sharp({
create: {
width: 1200,
height: 800,
channels: 3,
background: "#38bdf8",
},
})
.jpeg()
.toBuffer();
const info = await assertImageUpload(input, "masa-profile.jpg");
assert.equal(info.mime, "image/jpeg");
assert.equal(info.width, 1200);
const safeName = safeImageName(info.mime);
assert.match(safeName, /^[a-f0-9-]+\.jpg$/);
const outDir = await mkdtemp(path.join(tmpdir(), "images-"));
const baseName = safeName.replace(/\.[^.]+$/, "");
const variants = await optimizeImage(input, outDir, baseName, false);
assert.equal(variants.length, 3);
assert.ok(variants.every((item) => item.bytes > 0));
const thumb = await sharp(
path.join(outDir, `${baseName}-thumb.webp`),
).metadata();
assert.equal(thumb.width, 320);
assert.equal(thumb.height, 320);
assert.equal(thumb.exif, undefined);
});
Manual check harus mencakup mobile width, slow network, file rusak, file besar, foto vertikal dari ponsel, PNG transparan, dan screenshot dengan teks kecil. Setelah itu minta Claude Code review khusus validasi, nama file, metadata, CPU, fallback, dan celah test.
CTA monetisasi dan catatan verifikasi
Pemrosesan gambar melindungi monetisasi ketika user percaya pada apa yang mereka lihat: gambar produk cepat, screenshot terbaca, avatar aman, dan OGP stabil. Mulai dari checklist Claude Code gratis, gunakan produk ClaudeCodeLab untuk prompt dan template reusable, lalu gunakan training / consultation jika tim ingin aturan upload dan review gate masuk ke repository nyata.
Pada 2 Juni 2026, Masa menguji flow ini di proyek Next.js kecil. Hasil terbaik muncul saat prompt menulis batasnya lebih dulu: resize browser opsional, validasi server wajib, AVIF opsional, dan nama asli tidak boleh publik. Prompt samar “buat upload gambar” menghasilkan kode lebih lemah: validasi file.type, nama asli publik, orientasi hilang, dan AVIF synchronous. Budget dan contoh kegagalan sebelum implementasi membuat kualitas jauh lebih baik.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.