Upload file — gambar profil, lampiran dokumen, thumbnail artikel — adalah fitur yang hampir selalu ada di aplikasi web. Express.js sendiri tidak bisa menangani multipart/form-data (format yang digunakan untuk upload file) secara bawaan. Kita butuh middleware bernama Multer.
Di artikel ini kita bangun fitur upload gambar lengkap dengan validasi tipe file, batasan ukuran, dan penyimpanan terorganisir.
Apa itu Multer?
Multer adalah middleware Node.js untuk menangani multipart/form-data. Multer menambahkan objek body dan file (atau files) ke objek req.
npm install multer
Upload Dasar — Simpan ke Disk
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// Konfigurasi storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/'); // folder tujuan
},
filename: (req, file, cb) => {
// Buat nama file unik: timestamp-namaasli.ext
const ext = path.extname(file.originalname);
const namaUnik = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
cb(null, namaUnik);
}
});
const upload = multer({ storage });
// Route upload satu file
app.post('/upload', upload.single('gambar'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'Tidak ada file yang diupload' });
}
res.json({
pesan: 'Upload berhasil',
file: {
nama: req.file.filename,
ukuran: req.file.size,
tipe: req.file.mimetype,
url: `/uploads/${req.file.filename}`
}
});
});
Pastikan folder uploads/ sudah ada, dan serve file statisnya:
app.use('/uploads', express.static('uploads'));
Validasi Tipe File dan Ukuran
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true); // terima file
} else {
cb(new Error('Tipe file tidak didukung. Hanya JPG, PNG, WebP, GIF.'), false);
}
};
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // maks 5MB
files: 5 // maks 5 file sekaligus
}
});
Upload Multiple File
// Upload beberapa file dengan field name yang sama
app.post('/upload/multiple', upload.array('foto', 5), (req, res) => {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'Tidak ada file' });
}
const files = req.files.map(f => ({
nama: f.filename,
ukuran: f.size,
url: `/uploads/${f.filename}`
}));
res.json({ pesan: `${files.length} file berhasil diupload`, files });
});
// Upload dari beberapa field berbeda
app.post('/upload/mixed', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'galeri', maxCount: 10 }
]), (req, res) => {
const avatar = req.files['avatar']?.[0];
const galeri = req.files['galeri'] || [];
res.json({
avatar: avatar ? `/uploads/${avatar.filename}` : null,
galeri: galeri.map(f => `/uploads/${f.filename}`)
});
});
Organisasi Folder Upload
Pisahkan file berdasarkan tipe atau tanggal agar mudah dikelola:
const fs = require('fs');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// Buat subfolder berdasarkan bulan/tahun
const now = new Date();
const folder = `uploads/${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}`;
// Buat folder jika belum ada
fs.mkdirSync(folder, { recursive: true });
cb(null, folder);
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
cb(null, `${Date.now()}${ext}`);
}
});
Upload dengan Data Form Sekaligus
Upload file sering disertai data teks (judul, deskripsi, dll.):
app.post('/artikel', upload.single('thumbnail'), (req, res) => {
// Data teks dari body
const { judul, konten, kategori } = req.body;
// File thumbnail
const thumbnail = req.file ? `/uploads/${req.file.filename}` : null;
if (!judul || !konten) {
// Hapus file yang sudah terlanjur diupload jika validasi gagal
if (req.file) fs.unlinkSync(req.file.path);
return res.status(422).json({ error: 'Judul dan konten wajib diisi' });
}
const artikel = { id: Date.now(), judul, konten, kategori, thumbnail };
res.status(201).json({ pesan: 'Artikel dibuat', data: artikel });
});
Menangani Error Multer
Error Multer tidak otomatis ditangani oleh error handler Express. Perlu penanganan khusus:
function uploadMiddleware(req, res, next) {
upload.single('gambar')(req, res, (err) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'Ukuran file melebihi batas 5MB' });
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({ error: 'Terlalu banyak file' });
}
return res.status(400).json({ error: err.message });
} else if (err) {
return res.status(400).json({ error: err.message });
}
next();
});
}
app.post('/upload', uploadMiddleware, (req, res) => {
res.json({ file: req.file });
});
Menyimpan ke Cloud (Cloudinary)
Untuk production, menyimpan file di server lokal kurang ideal. Integrasi dengan Cloudinary memungkinkan file tersimpan di CDN cloud:
npm install cloudinary multer-storage-cloudinary
const cloudinary = require('cloudinary').v2;
const { CloudinaryStorage } = require('multer-storage-cloudinary');
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
});
const cloudStorage = new CloudinaryStorage({
cloudinary,
params: {
folder: 'blog-uploads',
allowed_formats: ['jpg', 'png', 'webp'],
transformation: [{ width: 1200, crop: 'limit', quality: 'auto' }]
}
});
const uploadCloud = multer({ storage: cloudStorage });
app.post('/upload/cloud', uploadCloud.single('gambar'), (req, res) => {
res.json({ url: req.file.path });
});
Kesimpulan
Multer membuat upload file di Express menjadi mudah dan fleksibel. Mulai dari penyimpanan lokal dengan nama file unik, validasi tipe dan ukuran, hingga integrasi cloud storage — semua bisa dikonfigurasi dengan bersih.
Di artikel terakhir seri ini, kita bahas cara deploy Express.js ke production dengan PM2 dan environment variables yang aman.
Kiki/🎮🍉⌨️🍩💻