express.static là middleware có sẵn dùng để phục vụ tệp từ một thư mục. app.use(express.static(path.join(__dirname, 'public'))) khiến mọi tệp trong public có mặt tại cùng đường dẫn trên URL: public/style.css trả lời /style.css, và public/index.html trả lời /. Express đặt Content-Type theo phần mở rộng của tệp, và khi không có tệp nào khớp, nó chuyển request đến các route của bạn thay vì trả về 404.
Câu trả lời cho GET /style.css là nội dung của tệp, kèm Content-Type: text/css; charset=utf-8 trong tab Headers. Gửi GET / để nhận index.html, rồi GET /api/time: không có tệp nào như vậy, nên route sẽ trả lời.
Vì sao đường dẫn thư mục cần __dirname
express.static('public') tìm 'public' tính từ thư mục bạn khởi chạy Node, không phải thư mục chứa server.js. Nó chạy được khi bạn gõ node server.js ngay trong dự án, và hỏng khi một trình quản lý tiến trình hoặc một script khởi động app từ nơi khác:
// project/server.js uses express.static('public')
// cd project && node server.js -> serves project/public
// node project/server.js -> looks for ./public, every file is 404
path.join(__dirname, 'public') là thư mục nằm cạnh tệp chứa dòng code đó, bất kể Node được khởi chạy từ đâu. Trong ES module, hãy dùng path.join(import.meta.dirname, 'public') (Node 20.11 trở lên). Trang module path trình bày chi tiết về __dirname và path.join.
Phục vụ tệp dưới một tiền tố
Truyền đường dẫn gắn trước để phục vụ thư mục dưới một tiền tố URL. Tiền tố này không thuộc đường dẫn của tệp trên ổ đĩa:
GET /assets/app.js trả về console.log("hi"); với kiểu text/javascript, và GET /assets/img/dot.svg truy cập vào thư mục con. Gửi GET /app.js không có tiền tố: kết quả là 404, vì các tệp chỉ có ở dưới /assets. Dùng tiền tố để URL của tệp không trùng với các route API.
Header cache với maxAge
Mặc định Express gửi Cache-Control: public, max-age=0, nên trình duyệt hỏi lại server mỗi lần dùng (và nhận 304 Not Modified rất nhanh khi tệp không đổi, nhờ header ETag). Với các tệp đổi tên sau mỗi lần build, như app.3f9a1c.js, hãy bảo trình duyệt giữ chúng lại:
Mở tab Headers: Cache-Control giờ là public, max-age=604800, tức bảy ngày tính bằng giây. index: false tắt việc phục vụ index.html cho URL trỏ đến thư mục. Các tùy chọn hữu ích khác là extensions: ['html'], giúp /about tìm được about.html, và dotfiles, mặc định là 'ignore' nên các tệp như .env không bao giờ bị phục vụ.
Single page app: quay về index.html
Một app React hoặc Vue dùng routing phía client cần mọi đường dẫn lạ đều trả về index.html, để khi tải lại trang /settings không bị 404. Đặt phần dự phòng này sau express.static và các route API:
app.use(express.static(path.join(__dirname, 'dist')));
app.use('/api', apiRouter);
// Express 5 wildcard syntax
app.get('/*splat', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
Lỗi thường gặp
Đưa tên thư mục vào URL. Với app.use(express.static('public')), URL là /style.css, không phải /public/style.css. Nếu muốn có /public trong URL, hãy gắn nó: app.use('/public', express.static(...)).
Liên kết tệp bằng đường dẫn tương đối trong HTML. <link href="style.css"> trên trang /blog/post sẽ yêu cầu /blog/style.css. Hãy bắt đầu đường dẫn bằng dấu gạch chéo: /style.css.
Đặt thông tin bí mật trong thư mục tĩnh. Mọi thứ trong đó đều công khai. Giữ tệp cấu hình, khóa và mã nguồn ở ngoài thư mục này.
Câu hỏi thường gặp
Có thể dùng express.static nhiều lần không?
Có. Đăng ký một lần cho mỗi thư mục; Express tìm trong chúng theo thứ tự bạn đã thêm và phục vụ tệp đầu tiên tìm thấy. Hãy cho mỗi thư mục một đường dẫn gắn riêng khi chúng có thể chứa tệp trùng tên.
Có nên để Express phục vụ tệp tĩnh trên production?
Với app nhỏ thì không sao. Khi lưu lượng lớn, hãy đặt một CDN hoặc reverse proxy như Nginx phía trước, chúng phục vụ tệp nhanh hơn và cache gần người dùng hơn, còn Express chỉ lo phần API.
Làm sao gửi một tệp cụ thể từ một route?
Dùng res.sendFile(path.join(__dirname, 'files', 'report.pdf')). Đường dẫn phải là tuyệt đối (hoặc bạn truyền tùy chọn root). Dùng res.download() khi trình duyệt nên lưu tệp thay vì hiển thị nó.