Skip to content
youhoc
  • Pages
    • Home
    • Modern App Guidelines
    • Linux
      • Day 1: Linux Distributions & Navigation
      • Day 2: User Management
      • Day 3: File Permission & Ownership
      • Day 4: Package Management
      • Day 5: Services Management
    • Javascript
      • JS The Weird Part
        • Execution Context
        • Types & Operators
        • Objects & Functions
        • Error Handling & Strict Mode
        • Typescript, ES6, Tra
      • Modern JS
        • JS in the Browser
        • Data Storage JSON
        • Modern JS
        • Advanced Objects & Methods
        • Webpack & Babel
        • Async
      • jQuery
        • In-depth Analysis of jQuery
      • React-ready JS
        • Arrow Function
        • Template Literals
        • Logical AND, OR, Ternary, Nullish Operators
        • Destructuring & Rest Operator
        • Array Method
        • Immutability and Spread Operator
        • Promises, Async/Await, Callback
    • PHP
      • gruntJS
      • composer
      • MySQL
    • Docker
      • Container Basics
      • Container Networking
      • Container Image
      • Container Volume & Persistent Data
      • Dockerfile
      • Docker Compose
      • Docker Registry
    • Node.js
      • Installing & Exploring
      • Loading Modules
      • npm - Get Command Input
      • Web Server
        • Express Web Server
        • Template Engine & MVC
      • File System & Input Arguments
      • 6. Deploy to Heroku & Github
      • Authentication
      • 7. Databases
      • 8. Rest API
      • Errors
      • Sequelize
    • File Upload với Multer, Express.js
    • ReactJS
      • React from Andrew
        • Summary from Next
        • 1. Basics
        • 2. React Components
        • 3. Webpack
        • 4. Styling with SCSS
        • 5. React Router
        • 6. React Hook
      • Modern React From The Beginning
        • Intro to JSX
        • Vite Build Tools
        • Basic Component Creation
        • Component State
        • Props & Component Composition
        • useState with Inputs & Form Submission
        • useEffect, useRef & Local Storage
        • Async / Await and Http Request in React
        • React Router: Declarative Mode
        • ContextAPI
        • React Router: Framework Mode
          • File-routing & HTML Layouts
          • Server-side Data Query
          • Links & Navigation
          • Loaders
    • Typescript
      • Type User vs UserProp
    • Payload CMS
    • Authentication

File Upload với Multer, Express.js

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:
image (file) → req.file
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ả destinationfilename đề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:
Method
Khi nào dùng
Memory
Performance
fs.readFile()
File nhỏ (<1MB), cần xử lý data
Cao
Thấp
fs.createReadStream()
File lớn, download, video
Thấp
Cao
res.download()
Download đơn giản
Thấp
Cao
res.sendFile()
Serve static, images, PDFs
Thấp
Cao
There are no rows in this table
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:
Tốn CPU mỗi lần generate
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:
Want to print your doc?
This is not the way.
Try clicking the ··· in the right corner or using a keyboard shortcut (
CtrlP
) instead.