Pengenalan
Node.js server dapat melayani static files dari folder assets/drive/ dengan endpoint yang lebih pendek dan clean. Express akan otomatis mendeteksi MIME type berdasarkan ekstensi file.
- ✅ Endpoint lebih pendek dan clean
- ✅ MIME type detection otomatis
- ✅ Support semua file types (images, videos, PDFs, dll)
- ✅ CORS enabled untuk cross-origin access
- ✅ Caching headers otomatis
- ✅ Performance tinggi dengan Express
Endpoint Mapping
File di folder assets/drive/ dapat diakses via endpoint /drive/:
| File Path | Node.js Endpoint |
|---|---|
assets/drive/images/logo.png |
http://localhost:3000/drive/images/logo.png |
assets/drive/2026/03/a1_xxx.png |
http://localhost:3000/drive/2026/03/a1_xxx.png |
assets/drive/documents/report.pdf |
http://localhost:3000/drive/documents/report.pdf |
assets/drive/videos/demo.mp4 |
http://localhost:3000/drive/videos/demo.mp4 |
Image Resize (On-the-fly)
Node.js server mendukung image resize otomatis dengan format URL: /drive/{width}x{height}/{path}
- ✅ Resize on-the-fly (tidak perlu pre-generate)
- ✅ Format:
/drive/{width}x{height}/{path} - ✅ High quality dengan Sharp library
- ✅ Cache 30 hari untuk performa
- ✅ Max dimensions: 5000x5000
- ✅ Fit mode: cover (crop to fit)
Contoh Resize
| Original | Resized | Dimensions |
|---|---|---|
/drive/images/logo.png |
/drive/200x200/images/logo.png |
200x200 px |
/drive/2026/03/a1_xxx.png |
/drive/500x300/2026/03/a1_xxx.png |
500x300 px |
/drive/images/hero.jpg |
/drive/1920x1080/images/hero.jpg |
1920x1080 px |
/drive/photos/avatar.png |
/drive/100x100/photos/avatar.png |
100x100 px (thumbnail) |
Penggunaan di HTML
<!-- Thumbnail 200x200 -->
<img src="http://localhost:3000/drive/200x200/images/logo.png" alt="Logo Thumbnail">
<!-- Medium 500x300 -->
<img src="http://localhost:3000/drive/500x300/2026/03/a1_xxx.png" alt="Medium Image">
<!-- Large 1920x1080 -->
<img src="http://localhost:3000/drive/1920x1080/images/hero.jpg" alt="Hero Image">
<!-- Original size (no resize) -->
<img src="http://localhost:3000/drive/images/logo.png" alt="Original Logo">Responsive Images dengan Resize
<picture>
<!-- Desktop: 1920x1080 -->
<source media="(min-width: 1024px)"
srcset="http://localhost:3000/drive/1920x1080/images/hero.jpg">
<!-- Tablet: 1024x768 -->
<source media="(min-width: 768px)"
srcset="http://localhost:3000/drive/1024x768/images/hero.jpg">
<!-- Mobile: 640x480 -->
<img src="http://localhost:3000/drive/640x480/images/hero.jpg"
alt="Hero Image">
</picture>Srcset untuk Retina Display
<img src="http://localhost:3000/drive/400x300/images/product.jpg"
srcset="http://localhost:3000/drive/400x300/images/product.jpg 1x,
http://localhost:3000/drive/800x600/images/product.jpg 2x"
alt="Product Image">Konfigurasi
Static file serving dikonfigurasi di server.js:
const express = require('express');
const path = require('path');
const fs = require('fs');
const sharp = require('sharp'); // Optional untuk image resize
// Image resize endpoint: /drive/{width}x{height}/{path}
app.get('/drive/:size(\\d+x\\d+)/:path(*)', async (req, res) => {
const [width, height] = req.params.size.split('x').map(Number);
const imagePath = path.join(__dirname, 'assets', 'drive', req.params.path);
// Resize dengan sharp
const resizedImage = await sharp(imagePath)
.resize(width, height, { fit: 'cover', position: 'center' })
.toBuffer();
res.set('Cache-Control', 'public, max-age=2592000');
res.send(resizedImage);
});
// Static files - serve from assets/drive as /drive (original)
app.use('/drive', express.static(path.join(__dirname, 'assets', 'drive')));Konfigurasi ini akan:
- Serve semua file dari
assets/drive/ - Mapping ke endpoint
/drive/* - Auto-detect MIME type
- Enable caching headers
- ✨ Image resize on-the-fly dengan format
/drive/{width}x{height}/{path}
Install Sharp (Optional)
Untuk enable image resize, install Sharp library:
# Install sharp
npm install sharp
# Restart server
pm2 restart nxdom-nodeSharp adalah library image processing tercepat untuk Node.js, menggunakan libvips untuk performa tinggi. Mendukung JPEG, PNG, WebP, GIF, AVIF, TIFF, dan SVG.
Supported File Types
| Category | Extensions | MIME Type |
|---|---|---|
| Images | .png, .jpg, .jpeg, .gif, .webp, .svg, .ico | image/* |
| Videos | .mp4, .webm, .ogg, .avi, .mov | video/* |
| Audio | .mp3, .wav, .ogg, .m4a | audio/* |
| Documents | .pdf, .doc, .docx, .xls, .xlsx | application/* |
| Text | .txt, .md, .csv | text/* |
| Archives | .zip, .rar, .7z, .tar, .gz | application/* |
| Web | .html, .css, .js, .json | text/html, text/css, etc |
Contoh Penggunaan
Menampilkan Gambar di HTML
<!-- Original size -->
<img src="http://localhost:3000/drive/images/logo.png" alt="Logo">
<!-- Resized 200x200 -->
<img src="http://localhost:3000/drive/200x200/images/logo.png" alt="Logo Thumbnail">
<!-- Resized 500x300 -->
<img src="http://localhost:3000/drive/500x300/2026/03/a1_xxx.png" alt="Medium Image">
<!-- Background image dengan resize -->
<div style="background-image: url('http://localhost:3000/drive/1920x1080/images/hero.jpg')">
</div>Fetch dari JavaScript
// Fetch image as blob
fetch('http://localhost:3000/drive/images/logo.png')
.then(res => res.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
document.getElementById('myImage').src = url;
});
// Fetch JSON data
fetch('http://localhost:3000/drive/data/config.json')
.then(res => res.json())
.then(data => console.log(data));
// Download file
async function downloadFile(path, filename) {
const response = await fetch(`http://localhost:3000/drive/${path}`);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
}
downloadFile('documents/report.pdf', 'report.pdf');Video Player
<video controls width="640" height="360">
<source src="http://localhost:3000/drive/videos/demo.mp4" type="video/mp4">
<source src="http://localhost:3000/drive/videos/demo.webm" type="video/webm">
Your browser doesn't support video tag.
</video>Audio Player
<audio controls>
<source src="http://localhost:3000/drive/audio/music.mp3" type="audio/mpeg">
<source src="http://localhost:3000/drive/audio/music.ogg" type="audio/ogg">
Your browser doesn't support audio tag.
</audio>PDF Viewer
<!-- Embed PDF -->
<embed src="http://localhost:3000/drive/documents/manual.pdf"
type="application/pdf"
width="100%"
height="600px">
<!-- iFrame PDF -->
<iframe src="http://localhost:3000/drive/documents/manual.pdf"
width="100%"
height="600px">
</iframe>Perbedaan PHP vs Node.js
Akses via PHP
Browser Request:
http://localhost/assets/drive/images/logo.png
↓
PHP Server (Apache/Nginx)
↓
.htaccess / routing
↓
ImagesController.php (jika ada processing)
↓
Serve file atau process imageKarakteristik:
- ✅ Dapat melakukan image processing (resize, crop, watermark)
- ✅ Access control & authentication
- ⚠️ Overhead PHP execution
- ⚠️ Lebih lambat untuk file besar
Akses via Node.js
Browser Request:
http://localhost:3000/drive/images/logo.png
↓
Node.js Server (Express)
↓
express.static middleware
↓
Direct file serve (no processing)Karakteristik:
- ✅ Sangat cepat (direct file serve)
- ✅ Efficient untuk file besar
- ✅ CORS enabled
- ✅ Caching headers otomatis
- ❌ Tidak ada image processing
- ❌ Tidak ada access control (public)
Kapan Menggunakan?
| Scenario | Rekomendasi | Alasan |
|---|---|---|
| Public images/assets | ✅ Node.js | Lebih cepat, tidak perlu processing |
| Image dengan resize/crop | ⚠️ PHP | Perlu image processing |
| Private files (auth required) | ⚠️ PHP | Perlu access control |
| Video streaming | ✅ Node.js | Efficient untuk file besar |
| CDN assets | ✅ Node.js | Fast serving, caching |
| User uploads (sensitive) | ⚠️ PHP | Perlu validation & security |
Contoh Lengkap
Image Gallery
<div id="gallery"></div>
<script>
const images = [
'drive/2026/03/a1_xxx.png',
'drive/2026/03/a2_xxx.png',
'drive/2026/03/a3_xxx.png'
];
const gallery = document.getElementById('gallery');
images.forEach(img => {
const imgElement = document.createElement('img');
imgElement.src = `http://localhost:3000/${img}`;
imgElement.alt = img;
imgElement.style.width = '200px';
imgElement.style.margin = '10px';
gallery.appendChild(imgElement);
});
</script>Lazy Loading Images
<img src="http://localhost:3000/drive/images/placeholder.png"
data-src="http://localhost:3000/drive/images/large-image.jpg"
loading="lazy"
alt="Large Image">Responsive Images
<picture>
<source media="(min-width: 1024px)"
srcset="http://localhost:3000/drive/images/hero-large.jpg">
<source media="(min-width: 768px)"
srcset="http://localhost:3000/drive/images/hero-medium.jpg">
<img src="http://localhost:3000/drive/images/hero-small.jpg"
alt="Hero Image">
</picture>Advanced Configuration
Resize Options
Customize resize behavior di server.js:
// fit: 'cover' - Crop to fill (default)
const resizedImage = await sharp(imagePath)
.resize(width, height, { fit: 'cover', position: 'center' })
.toBuffer();
// fit: 'contain' - Fit inside (no crop, add padding)
const resizedImage = await sharp(imagePath)
.resize(width, height, { fit: 'contain', background: '#ffffff' })
.toBuffer();
// fit: 'fill' - Stretch to fill (distort)
const resizedImage = await sharp(imagePath)
.resize(width, height, { fit: 'fill' })
.toBuffer();
// fit: 'inside' - Resize to fit inside (maintain aspect)
const resizedImage = await sharp(imagePath)
.resize(width, height, { fit: 'inside' })
.toBuffer();Quality & Format
// High quality JPEG
const resizedImage = await sharp(imagePath)
.resize(width, height)
.jpeg({ quality: 90 })
.toBuffer();
// WebP format (smaller size)
const resizedImage = await sharp(imagePath)
.resize(width, height)
.webp({ quality: 80 })
.toBuffer();
// PNG with compression
const resizedImage = await sharp(imagePath)
.resize(width, height)
.png({ compressionLevel: 9 })
.toBuffer();Custom Cache Headers
// Static files dengan custom cache
app.use('/drive', express.static(path.join(__dirname, 'assets', 'drive'), {
maxAge: '1d', // Cache 1 hari
etag: true, // Enable ETag
lastModified: true
}));Multiple Static Folders
// Serve multiple folders
app.use('/drive', express.static(path.join(__dirname, 'assets', 'drive')));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
app.use('/public', express.static(path.join(__dirname, 'public')));Index File Support
// Serve index.html untuk directory requests
app.use('/drive', express.static(path.join(__dirname, 'assets', 'drive'), {
index: 'index.html'
}));
// Request: http://localhost:3000/drive/gallery/
// Serve: assets/drive/gallery/index.htmlDisable Directory Listing
// Disable directory listing (default sudah disabled)
app.use('/drive', express.static(path.join(__dirname, 'assets', 'drive'), {
index: false, // Tidak serve index.html
dotfiles: 'deny' // Block .htaccess, .env, dll
}));Security
Restrict File Types
// Middleware untuk filter file types
app.use('/drive', (req, res, next) => {
const allowedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.pdf', '.mp4'];
const ext = path.extname(req.path).toLowerCase();
if (!allowedExtensions.includes(ext)) {
return res.status(403).json({ error: 'File type not allowed' });
}
next();
});
app.use('/drive', express.static(path.join(__dirname, 'assets', 'drive')));Rate Limiting
const rateLimit = require('express-rate-limit');
const fileRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 menit
max: 1000 // max 1000 requests per window
});
app.use('/drive', fileRateLimiter);Authentication (Optional)
// Protect specific folders
app.use('/drive/private', (req, res, next) => {
const token = req.headers['authorization'];
if (!token || !verifyToken(token)) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
app.use('/drive/private', express.static(path.join(__dirname, 'assets', 'drive', 'private')));Performance Optimization
Compression
const compression = require('compression');
// Enable gzip compression
app.use(compression());
// Static files akan di-compress otomatisCache Control
// Long cache untuk static assets
app.use('/drive', express.static(path.join(__dirname, 'assets', 'drive'), {
maxAge: '365d', // Cache 1 tahun
immutable: true // File tidak akan berubah
}));CDN Integration
Untuk production, gunakan CDN untuk static files:
const CDN_URL = process.env.CDN_URL || 'http://localhost:3000';
function getAssetUrl(path) {
return `${CDN_URL}/drive/${path}`;
}
// Usage
const imageUrl = getAssetUrl('images/logo.png');
// Development: http://localhost:3000/drive/images/logo.png
// Production: https://cdn.yourdomain.com/drive/images/logo.pngTroubleshooting
File not found (404)
Cannot GET /drive/images/notfound.pngSolusi:
- Check file exists:
dir assets\drive\images\notfound.png - Check path case-sensitive (Linux/macOS)
- Check file permissions
CORS Error
Access to image at 'http://localhost:3000/drive/...' from origin
'http://localhost' has been blocked by CORS policySolusi: CORS sudah enabled di server.js dengan app.use(cors());
Large File Timeout
Untuk file sangat besar (>100MB), tambahkan timeout:
// Increase timeout untuk large files
app.use('/drive', (req, res, next) => {
req.setTimeout(300000); // 5 minutes
res.setTimeout(300000);
next();
});
app.use('/drive', express.static(path.join(__dirname, 'assets', 'drive')));Best Practices
- Organize files by type - Gunakan folder structure yang jelas (images/, videos/, documents/)
- Use descriptive filenames - Avoid special characters dan spaces
- Optimize images - Compress sebelum upload untuk performance
- Set proper cache headers - Long cache untuk static assets
- Use CDN for production - Offload static files ke CDN
- Monitor disk usage - Regular cleanup untuk old files
- Backup important files - Regular backup untuk user uploads
Monitoring
File Access Logs
const morgan = require('morgan');
// Log semua static file requests
app.use('/drive', morgan('combined'));
app.use('/drive', express.static(path.join(__dirname, 'assets', 'drive')));Custom Logging
// Log file access dengan custom format
app.use('/drive', (req, res, next) => {
console.log(`[Static] ${req.method} ${req.url} - ${req.ip}`);
next();
});
app.use('/drive', express.static(path.join(__dirname, 'assets', 'drive')));Production Deployment
Nginx Static Files
Untuk production, serve static files langsung dari Nginx (lebih cepat):
server {
listen 80;
server_name yourdomain.com;
# Static files langsung dari Nginx
location /drive/ {
alias /var/www/nexaui/assets/drive/;
expires 365d;
add_header Cache-Control "public, immutable";
}
# API requests ke Node.js
location /api/ {
proxy_pass http://localhost:3000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
# Fallback ke Node.js
location / {
proxy_pass http://localhost:3000;
}
}CloudFlare CDN
Setup CloudFlare sebagai CDN:
- Point domain ke server IP
- Enable CloudFlare proxy (orange cloud)
- Set cache rules untuk
/drive/* - Enable "Cache Everything" untuk static assets
File Upload (Bonus)
Untuk handle file upload via Node.js, gunakan multer:
npm install multerconst multer = require('multer');
// Configure storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.join(__dirname, 'assets', 'drive', 'uploads'));
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '_' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '_' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB max
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/png', 'image/jpeg', 'image/gif'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
}
});
// Upload endpoint
app.post('/api/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
message: 'File uploaded successfully',
filename: req.file.filename,
url: `http://localhost:3000/drive/uploads/${req.file.filename}`
});
});Testing
Test Image Loading
<!DOCTYPE html>
<html>
<head>
<title>Test Static Files</title>
</head>
<body>
<h1>Test Static Files</h1>
<h2>Images</h2>
<img src="http://localhost:3000/drive/images/logo.png" width="200">
<img src="http://localhost:3000/drive/2026/03/a1_xxx.png" width="200">
<h2>Video</h2>
<video controls width="400">
<source src="http://localhost:3000/drive/videos/demo.mp4" type="video/mp4">
</video>
<h2>PDF</h2>
<embed src="http://localhost:3000/drive/documents/manual.pdf" width="100%" height="500px">
</body>
</html>Test dengan JavaScript
// Test image load
async function testImageLoad() {
try {
const response = await fetch('http://localhost:3000/drive/images/logo.png');
console.log('Status:', response.status);
console.log('Content-Type:', response.headers.get('Content-Type'));
console.log('Content-Length:', response.headers.get('Content-Length'));
const blob = await response.blob();
console.log('Blob size:', blob.size, 'bytes');
console.log('Blob type:', blob.type);
} catch (error) {
console.error('Error:', error);
}
}
testImageLoad();Comparison: PHP vs Node.js
| Aspek | PHP (ImagesController) | Node.js (Sharp) |
|---|---|---|
| Speed | ⚠️ Slower (GD library) | ✅ Faster (libvips/Sharp) |
| Resize | ✅ Query param (?w=500&h=300) | ✅ URL path (/500x300/image.png) |
| Quality | ✅ Good (GD library) | ✅ Excellent (Sharp/libvips) |
| Memory | ⚠️ High memory usage | ✅ Low memory (streaming) |
| Format Support | PNG, JPG, GIF, WebP | PNG, JPG, GIF, WebP, AVIF, TIFF |
| Authentication | ✅ Built-in session/auth | ⚠️ Custom middleware |
| CORS | ⚠️ Manual headers | ✅ Built-in middleware |
| Caching | ✅ ETag + Cache-Control | ✅ ETag + Cache-Control |
Performance Benchmark
| Operation | PHP (GD) | Node.js (Sharp) |
|---|---|---|
| Resize 1920x1080 → 500x300 | ~200ms | ~50ms |
| Resize 4000x3000 → 1024x768 | ~800ms | ~150ms |
| Memory usage (1 image) | ~50MB | ~20MB |
Advanced Resize Features
Watermark
// Resize dengan watermark
app.get('/drive/:size(\\d+x\\d+)/:path(*)', async (req, res) => {
const [width, height] = req.params.size.split('x').map(Number);
const imagePath = path.join(__dirname, 'assets', 'drive', req.params.path);
const watermarkPath = path.join(__dirname, 'assets', 'watermark.png');
const resizedImage = await sharp(imagePath)
.resize(width, height, { fit: 'cover' })
.composite([{
input: watermarkPath,
gravity: 'southeast' // Bottom right
}])
.toBuffer();
res.send(resizedImage);
});Format Conversion
// Auto-convert ke WebP untuk browser yang support
app.get('/drive/:size(\\d+x\\d+)/:path(*)', async (req, res) => {
const [width, height] = req.params.size.split('x').map(Number);
const imagePath = path.join(__dirname, 'assets', 'drive', req.params.path);
const acceptsWebP = req.headers.accept?.includes('image/webp');
let image = sharp(imagePath).resize(width, height);
if (acceptsWebP) {
image = image.webp({ quality: 80 });
res.set('Content-Type', 'image/webp');
} else {
res.set('Content-Type', 'image/jpeg');
}
const buffer = await image.toBuffer();
res.send(buffer);
});Blur & Effects
// Resize dengan blur effect
const resizedImage = await sharp(imagePath)
.resize(width, height)
.blur(5) // Blur radius
.toBuffer();
// Grayscale
const resizedImage = await sharp(imagePath)
.resize(width, height)
.grayscale()
.toBuffer();
// Rotate
const resizedImage = await sharp(imagePath)
.resize(width, height)
.rotate(90)
.toBuffer();Example: Complete Image Service
// Static files dengan logging dan security
app.use('/drive',
// 1. Logging
(req, res, next) => {
console.log(`[File] ${req.method} ${req.url}`);
next();
},
// 2. Security - block dotfiles
(req, res, next) => {
if (req.path.includes('/.')) {
return res.status(403).send('Forbidden');
}
next();
},
// 3. Serve files
express.static(path.join(__dirname, 'assets', 'drive'), {
maxAge: '1d',
etag: true,
lastModified: true
})
);Quick Reference
| URL Format | Contoh | Hasil |
|---|---|---|
/drive/{path} |
/drive/images/logo.png |
Original size |
/drive/{w}x{h}/{path} |
/drive/500x300/images/logo.png |
Resized 500x300 |
/drive/{w}x{h}/{path} |
/drive/200x200/2026/03/a1.png |
Resized 200x200 |
/drive/{w}x{h}/{path} |
/drive/1920x1080/images/hero.jpg |
Resized 1920x1080 |
Next Steps
- Install Sharp:
npm install sharp - Test resize:
http://localhost:3000/drive/500x300/images/logo.png - Setup CDN untuk production
- Implementasi file upload dengan multer
- Tambahkan watermark untuk protected images
- Setup backup strategy untuk user uploads
- Monitor disk usage dan cleanup old files