Vấn đề khi upload file với Body Parser
Khi bắt đầu xây dựng tính năng upload ảnh, bạn có thể nghĩ đơn giản là thay thế imageURL (string) bằng một input file picker để nhận file từ người dùng:
Tuy nhiên, khi thử console.log(req.body.image), bạn sẽ gặp vấn đề: không thể in ra được nội dung file. Tại sao?
Nguyên nhân
Express mặc định sử dụng middleware body-parser với cấu hình:
Middleware urlencoded chỉ xử lý dữ liệu dạng text và chuyển tất cả form data thành string. Trong khi đó, file ảnh là dữ liệu binary - một định dạng hoàn toàn khác mà body-parser không thể xử lý.
Giải pháp: Multer
Để upload file, bạn cần:
Thay đổi encoding type của form thành multipart/form-data:
Cài đặt và cấu hình Multer:
Cách Multer hoạt động
Một điểm quan trọng cần hiểu: Multer chạy song song với các middleware khác của Express. Multer chỉ chịu trách nhiệm xử lý file bên trong form, còn các input text thông thường vẫn được parse bình thường bởi body-parser.
Khi request đến:
Multer xử lý file và đưa vào req.file (hoặc req.files nếu upload nhiều file) Body Parser vẫn parse các field text và đưa vào req.body Ví dụ trong form trên:
title, description (text) → req.body.title, req.body.description Cấu hình Multer chi tiết
1. Thiết lập Multer middleware
Sau khi cài đặt Multer, bạn cần thêm middleware vào ứng dụng Express, đặt ngay sau body-parser:
Các phương thức upload của Multer:
.single('fieldname') - Upload 1 file duy nhất với tên field được chỉ định. File sẽ được lưu trong req.file .array('fieldname', maxCount) - Upload nhiều file cùng 1 field name. Files sẽ được lưu trong req.files (mảng) .fields([{name: 'field1', maxCount: 1}, {name: 'field2', maxCount: 2}]) - Upload nhiều file từ nhiều field khác nhau .none() - Chỉ chấp nhận text fields, không cho phép file nào .any() - Chấp nhận tất cả file từ mọi field (không khuyến khích vì thiếu kiểm soát) 2. Cấu trúc dữ liệu file trong req.file
Khi console.log(req.file), bạn sẽ thấy object chứa thông tin file:
Đây là cách Multer “đóng gói” file binary thành object JavaScript để bạn có thể làm việc dễ dàng hơn.
3. Cấu hình đơn giản với dest
Cách nhanh nhất để lưu file là dùng tùy chọn dest:
File sẽ được lưu vào thư mục images/ với tên ngẫu nhiên (không có extension). Tuy nhiên, cách này hạn chế vì bạn không kiểm soát được tên file.
4. Cấu hình nâng cao với diskStorage
Để kiểm soát chi tiết hơn về nơi lưu và cách đặt tên file, sử dụng diskStorage:
Giải thích callback pattern:
Cả destination và filename đều nhận 3 tham số:
req - Request object, chứa thông tin về request hiện tại file - Object chứa thông tin về file đang được upload cb - Callback function để trả kết quả về cho Multer Tại sao phải dùng callback?
Multer xử lý file bất đồng bộ (asynchronous). Callback cho phép bạn:
Thực hiện các tác vụ async (ví dụ: kiểm tra database, tạo thư mục động) Báo lỗi nếu có vấn đề: cb(error) Trả về kết quả thành công: cb(null, result) Cú pháp callback:
Tham số đầu tiên: error (null nếu không có lỗi) Tham số thứ hai: result (giá trị trả về) Ví dụ nâng cao hơn:
Lưu ý quan trọng:
Luôn gọi cb() để Multer biết bạn đã hoàn thành Không gọi cb() sẽ khiến request bị “treo” Pattern này tránh “callback hell” bằng cách chỉ lồng 1 level, không deep nesting Lọc file với fileFilter và thông báo lỗi
Để đảm bảo người dùng chỉ upload đúng loại file mong muốn, Multer cung cấp tùy chọn fileFilter:
Cách hoạt động:
cb(null, true) - Chấp nhận file, Multer sẽ tiếp tục xử lý cb(null, false) - Từ chối file, Multer bỏ qua file này (không lưu, không báo lỗi) cb(new Error('message')) - Từ chối và throw error Hiển thị thông báo lỗi khi file không hợp lệ
Cách 1: Throw error trực tiếp trong fileFilter
Cách 2: Xử lý lỗi trong route handler (linh hoạt hơn)
Cách 3: Sử dụng error handling middleware
Các loại filter khác
1. Giới hạn kích thước file
Khi file vượt quá giới hạn, Multer sẽ throw MulterError với code LIMIT_FILE_SIZE.
2. Giới hạn số lượng file
3. Các giới hạn khác
4. Filter tổng hợp (Best Practice)
Lưu ý bảo mật:
Luôn kiểm tra cả MIME type VÀ file extension Không tin tưởng hoàn toàn vào file.mimetype vì có thể bị giả mạo Nên thêm virus scanning cho production app Lưu file upload bên ngoài document root để tránh execute file độc hại Lưu trữ và phục vụ file sau khi upload
1. Lưu thông tin file vào Database
Sau khi upload thành công, bạn nên lưu thông tin file vào database thay vì chỉ lưu trên disk:
Lưu ý:
imagePath - Đường dẫn vật lý trên server (images/1234567890-photo.jpg) imageUrl - URL public để truy cập (/images/1234567890-photo.jpg) Nên lưu cả hai để linh hoạt trong xử lý 2. Serve Public Files (Static Files)
Để cho phép truy cập trực tiếp file qua URL, sử dụng Express static middleware:
Người dùng có thể truy cập: http://localhost:3000/images/1234567890-photo.jpg
Best Practice cho Public Files:
Đặt thư mục static bên ngoài source code (/public/uploads thay vì /src/uploads) Sử dụng CDN cho production để giảm tải server Set cache headers để tối ưu performance 3. Serve Private Files (Protected Routes)
Đối với file cần bảo mật (invoice, documents cá nhân), không để public mà tạo route có authentication:
4. Cách trả file về client
Express cung cấp nhiều method để serve file, mỗi method phù hợp với mục đích khác nhau:
a) res.download() - Download file ngay
b) res.sendFile() - Hiển thị file trên trình duyệt
c) Custom headers (kiểm soát chi tiết nhất)
Best Practice cho File Headers:
5. Streaming vs Preloading Data
❌ KHÔNG NÊN: Preloading (fs.readFile)
Vấn đề:
File lớn (video, backup) sẽ làm tràn RAM Blocking operation, server chậm User phải đợi load hết file mới nhận được byte đầu tiên ✅ NÊN DÙNG: Streaming (fs.createReadStream)
Ưu điểm Streaming:
Chỉ load từng chunk nhỏ vào memory (thường 64KB/chunk) User nhận được data ngay lập tức, không phải đợi Hỗ trợ pause/resume tự động Server có thể handle nhiều download đồng thời Advanced: Streaming với Range Requests (cho video)
Tóm tắt:
Best Practice cuối cùng:
Luôn dùng streaming cho files > 1MB Set đúng Content-Type để browser biết cách xử lý Implement rate limiting cho download endpoints Log mọi file access để audit security Clean up orphan files (files không còn reference trong DB) Backup files quan trọng định kỳ Tạo PDF động với PDFKit
Thay vì lưu trữ hàng ngàn file PDF invoice trên server, bạn có thể tạo PDF on-the-fly (động) mỗi khi user yêu cầu tải về.
1. Cài đặt PDFKit
2. Tạo và serve PDF trực tiếp
Cách hoạt động:
pdfDoc.pipe(fs.createWriteStream(invoicePath)) - Lưu PDF vào disk (backup) pdfDoc.pipe(res) - Stream trực tiếp đến client User nhận PDF ngay lập tức mà không cần đợi file được tạo hoàn toàn Sử dụng streaming nên hiệu quả với memory 3. Tạo PDF mà KHÔNG lưu trữ (Pure On-the-fly)
Nếu bạn không muốn lưu file PDF trên server:
Ưu điểm:
Tiết kiệm dung lượng disk Không cần quản lý file cleanup Luôn tạo PDF mới nhất dựa trên data hiện tại Nhược điểm:
Không có backup nếu data bị xóa 4. Xóa file PDF sau khi download (hoặc sau thời gian)
Cách 1: Xóa ngay sau khi response hoàn tất
Cách 2: Xóa sau một khoảng thời gian (ví dụ: 1 giờ)
Cách 3: Scheduled cleanup job (Best Practice cho production)
5. Tạo Invoice PDF chi tiết hơn
Best Practices
1. Chọn strategy phù hợp:
Pure on-the-fly: Cho invoices, receipts (data thay đổi thường xuyên) Save + scheduled cleanup: Cho reports, certificates (cần backup ngắn hạn) Permanent storage: Cho legal documents (cần lưu vĩnh viễn) 2. Performance:
3. Security:
4. Error handling: