# Bài 04 — Khởi tạo dự án Express
⬅️ Bài trước | Mục lục | Bài tiếp theo ➡️
🎯 Mục tiêu
- Tạo dự án backend bằng
express-generator - Hiểu vai trò của
app.js,bin/www,routes/ - Hiểu middleware — khái niệm cốt lõi của Express
- Bật CORS để frontend gọi được backend
- Đổi port sang 5000 để không đụng Next.js
- Chạy được server và thấy phản hồi trong trình duyệt
📚 1. Express là gì?
Node.js cho phép tạo web server, nhưng code rất dài dòng:
// Node.js thuần — chỉ để trả về "Hello"
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/products' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify([{ name: 'Gucci' }]));
} else {
res.writeHead(404);
res.end();
}
});
server.listen(5000);
Express bọc lại cho gọn:
// Với Express
const express = require('express');
const app = express();
app.get('/products', (req, res) => {
res.json([{ name: 'Gucci' }]);
});
app.listen(5000);
Express lo giúp bạn: phân tích URL, đọc body JSON, phục vụ file tĩnh, xử lý lỗi.
💻 2. Tạo dự án bằng express-generator
express-generator là công cụ sinh sẵn cấu trúc thư mục chuẩn.
Từ thư mục gốc lith-perfume/:
npx express-generator --view=ejs backend
Giải thích:
npx— chạy gói mà không cần cài toàn cục--view=ejs— dùng EJS làm view engine (dự án chỉ dùng nó cho trang lỗi)backend— tên thư mục sẽ tạo
Kết quả:
backend/
├── app.js
├── package.json
├── bin/
│ └── www
├── public/
│ ├── images/
│ ├── javascripts/
│ └── stylesheets/
│ └── style.css
├── routes/
│ ├── index.js
│ └── users.js
└── views/
├── error.ejs
└── index.ejs
Cài các thư viện
cd backend
npm install
Lệnh này đọc package.json và tải mọi thư viện vào node_modules/.
Cài thêm 4 gói dự án cần:
npm install mongodb cors multer
npm install --save-dev nodemon
| Gói | Công dụng |
|---|---|
mongodb |
Driver chính thức để kết nối MongoDB |
cors |
Cho phép frontend (port 3000) gọi backend (port 5000) |
multer |
Xử lý upload file (bài 07) |
nodemon |
Tự khởi động lại server khi bạn sửa code |
💻 3. File package.json
Mở backend/package.json và sửa phần scripts:
{
"name": "backend",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www",
"dev": "nodemon ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"ejs": "~2.6.1",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"mongodb": "^6.7.0",
"morgan": "~1.9.1",
"multer": "^1.4.5-lts.1",
"nodemon": "^3.1.4"
}
}
Từ giờ bạn chạy backend bằng:
npm run dev
nodemon sẽ theo dõi file và tự restart mỗi khi bạn lưu — không phải Ctrl+C rồi chạy lại nữa.
💡
dependencieslà thư viện cần khi chạy thật.devDependencieslà công cụ chỉ dùng khi phát triển (như nodemon). Dự án gốc đểnodemontrongdependencies— không sai nghiêm trọng nhưng không chuẩn.
📚 4. Hiểu file bin/www — điểm khởi động
Đây là file thực sự chạy đầu tiên. Nó có nhiệm vụ: lấy port, tạo HTTP server, lắng nghe.
Mở backend/bin/www, phần quan trọng nhất là:
var app = require('../app');
var http = require('http');
// Lấy port từ biến môi trường, mặc định là 3000
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
var server = http.createServer(app);
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
⚠️ VẤN ĐỀ QUAN TRỌNG: Port 3000 bị trùng
Next.js cũng chạy ở port 3000. Nếu để nguyên, khi bật cả hai bạn sẽ gặp:
Error: listen EADDRINUSE: address already in use :::3000
Đây là lỗi đầu tiên mà 100% người mới gặp khi làm dự án này.
✅ Cách sửa — đổi backend sang port 5000
Mở backend/bin/www, sửa dòng:
// TRƯỚC
var port = normalizePort(process.env.PORT || '3000');
// SAU
var port = normalizePort(process.env.PORT || '5000');
Và thêm log cho dễ nhìn — sửa hàm onListening ở cuối file:
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
console.log(`🚀 Server đang chạy tại http://localhost:${addr.port}`);
}
Nội dung đầy đủ backend/bin/www
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('backend:server');
var http = require('http');
/**
* Lấy port từ biến môi trường, mặc định 5000.
* (KHÔNG dùng 3000 vì Next.js đã chiếm port đó)
*/
var port = normalizePort(process.env.PORT || '5000');
app.set('port', port);
/**
* Tạo HTTP server.
*/
var server = http.createServer(app);
/**
* Lắng nghe.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Chuẩn hóa port về dạng number/string/false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) return val; // named pipe
if (port >= 0) return port; // port number
return false;
}
/**
* Xử lý sự kiện "error" của server.
*/
function onError(error) {
if (error.syscall !== 'listen') throw error;
var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Xử lý sự kiện "listening" của server.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
debug('Listening on ' + bind);
console.log(`🚀 Server đang chạy tại http://localhost:${addr.port}`);
}
💡
process.env.PORT ||nghĩa là: "nếu có biến môi trường PORT thì dùng nó, không thì dùng 5000". Khi deploy lên Render/Heroku (bài 22), nhà cung cấp sẽ tự đặtPORT— đây là lý do dòng này quan trọng.
📚 5. Middleware — khái niệm cốt lõi của Express
Middleware là một hàm nằm giữa request và response. Mỗi request đi qua một "dây chuyền" middleware theo đúng thứ tự bạn khai báo.
Request đến
│
▼
┌────────────────┐
│ cors() │ → gắn header cho phép frontend gọi
└────────────────┘
│ next()
▼
┌────────────────┐
│ logger('dev') │ → in ra console: GET /products 200 15ms
└────────────────┘
│ next()
▼
┌────────────────┐
│ express.json() │ → đọc body JSON, gán vào req.body
└────────────────┘
│ next()
▼
┌────────────────┐
│ router │ → tìm route khớp, chạy hàm xử lý
└────────────────┘
│
▼
Response trả về
Cấu trúc một middleware
function middlewareCuaToi(req, res, next) {
console.log("Có request tới:", req.url);
next(); // ← BẮT BUỘC gọi next() để chuyển sang middleware tiếp theo
}
app.use(middlewareCuaToi);
⚠️ Quên gọi
next()→ request bị treo vĩnh viễn, trình duyệt quay vòng mãi. Trừ khi bạn đã gọires.json()/res.send()để kết thúc luôn.
Ba tham số quen thuộc
| Tham số | Là gì | Dùng nhiều nhất |
|---|---|---|
req |
Thông tin yêu cầu từ client | req.params, req.body, req.query, req.file |
res |
Đối tượng để trả lời | res.json(), res.status(), res.send() |
next |
Hàm chuyển sang bước tiếp | next() hoặc next(err) |
💻 6. Viết lại app.js
Đây là file cấu hình trung tâm. Thay toàn bộ nội dung backend/app.js bằng:
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var cors = require('cors');
// ===== Import các router =====
// (sẽ tạo ở bài 05, 06, 08 — tạm comment nếu chưa có file)
var categoriesRouter = require('./routes/categories');
var productsRouter = require('./routes/products');
var orderRouter = require('./routes/oder');
var app = express();
// ===== CORS: cho phép frontend ở port khác gọi vào =====
app.use(cors());
// ===== View engine (chỉ dùng để render trang lỗi) =====
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// ===== Các middleware chung =====
app.use(logger('dev')); // log request ra console
app.use(express.json()); // đọc body dạng JSON
app.use(express.urlencoded({ extended: false })); // đọc body dạng form
app.use(cookieParser()); // đọc cookie
app.use(express.static(path.join(__dirname, 'public')));// phục vụ file tĩnh
// ===== Gắn router vào đường dẫn =====
app.use('/categories', categoriesRouter);
app.use('/products', productsRouter);
app.use('/orders', orderRouter);
// ===== Bắt 404: không route nào khớp =====
app.use(function (req, res, next) {
next(createError(404));
});
// ===== Xử lý lỗi tập trung =====
app.use(function (err, req, res, next) {
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
🔍 Giải thích từng khối
app.use(cors())
Trình duyệt có chính sách bảo mật Same-Origin Policy: trang web ở
http://localhost:3000 không được phép gọi http://localhost:5000 (khác port = khác origin).
cors() gắn thêm header Access-Control-Allow-Origin: * vào mọi response,
báo cho trình duyệt "tôi cho phép".
Nếu quên dòng này, frontend sẽ báo:
Access to fetch at 'http://localhost:5000/products' from origin 'http://localhost:3000'
has been blocked by CORS policy
⚠️
cors()không tham số nghĩa là cho phép TẤT CẢ domain. Tiện khi học, nguy hiểm khi chạy thật. Bài 22 sẽ hướng dẫn giới hạn:app.use(cors({ origin: 'https://lith-perfume.vercel.app' }));
app.use(express.json())
Không có dòng này, req.body sẽ là undefined khi client gửi JSON.
Đây là nguyên nhân của lỗi "req.body is undefined" mà rất nhiều người mới gặp.
app.use(express.static(path.join(__dirname, 'public')))
Mọi file trong backend/public/ được phục vụ trực tiếp qua HTTP:
| File trên đĩa | URL truy cập |
|---|---|
backend/public/img/sp1.jpg |
http://localhost:5000/img/sp1.jpg |
backend/public/stylesheets/style.css |
http://localhost:5000/stylesheets/style.css |
Frontend sẽ dùng đường dẫn này để hiển thị ảnh sản phẩm.
__dirname là biến của Node, cho ra đường dẫn tuyệt đối tới thư mục chứa app.js.
Dùng path.join() để ghép đường dẫn chạy đúng trên cả Windows (\) lẫn Linux (/).
Thứ tự middleware rất quan trọng
app.use(express.json()); // 1. Phải đứng TRƯỚC router
app.use('/products', productsRouter); // 2. Thì router mới có req.body
app.use(function (req, res, next) { // 3. 404 phải đứng SAU tất cả router
next(createError(404));
});
Nếu bạn để middleware 404 lên đầu, mọi request đều 404.
Middleware xử lý lỗi có 4 tham số
app.use(function (err, req, res, next) { ... });
// ↑ tham số đầu là err
Express nhận diện middleware xử lý lỗi bằng số lượng tham số. Viết 3 tham số → Express coi là middleware thường, lỗi không được bắt.
⚠️ 7. Lỗi trong source gốc
Lỗi 1 — routes/index.js là code chết
File backend/routes/index.js có 6 route được viết cẩn thận:
router.get("/", ...) // danh sách sản phẩm
router.get('/products/:id', ...) // chi tiết sản phẩm
router.get('/user', ...) // (thực ra trả về categories?!)
router.get('/productbycate/:id', ...) // sản phẩm theo danh mục
router.get('/products/hot', ...) // sản phẩm hot
router.get('/search/:keyword', ...) // tìm kiếm
Nhưng trong app.js gốc, cả hai dòng liên quan đều bị comment:
// var userRouter = require('./routes/index');
// app.use('/', userRouter);
→ Không route nào trong file này chạy được. Bao gồm cả route tìm kiếm — đó là lý do ô search trên navbar không hoạt động.
Xử lý: tài liệu này bỏ hẳn routes/index.js, và xây lại route tìm kiếm
đúng chỗ ở bài 21. Bạn có thể xóa file đó đi.
Lỗi 2 — routes/users.js chỉ là mẫu
router.get('/', function (req, res, next) {
res.send('respond with a resource');
});
Đây là file mẫu do generator sinh ra, không có nội dung thật. Xóa được.
Lỗi 3 — Tên file oder.js sai chính tả
Đúng phải là order.js. Tài liệu giữ nguyên tên oder.js để bạn đối chiếu
được với source gốc. Nếu muốn sửa, nhớ sửa cả dòng require trong app.js.
✅ 8. Chạy thử
Vì các file router chưa tồn tại, hãy tạm comment 3 dòng import và 3 dòng app.use:
// var categoriesRouter = require('./routes/categories');
// var productsRouter = require('./routes/products');
// var orderRouter = require('./routes/oder');
// app.use('/categories', categoriesRouter);
// app.use('/products', productsRouter);
// app.use('/orders', orderRouter);
Và thêm tạm một route để test:
app.get('/ping', (req, res) => {
res.json({ message: 'Backend đang chạy!', time: new Date() });
});
Chạy server:
cd backend
npm run dev
Kết quả mong đợi:
[nodemon] starting `node ./bin/www`
🚀 Server đang chạy tại http://localhost:5000
Mở trình duyệt vào http://localhost:5000/ping, bạn phải thấy:
{ "message": "Backend đang chạy!", "time": "2026-08-16T..." }
Thử vào một URL không tồn tại như http://localhost:5000/abc — phải thấy trang lỗi 404
(do middleware createError(404) + views/error.ejs).
Nhìn vào terminal, morgan in ra từng request:
GET /ping 200 3.421 ms - 63
GET /abc 404 12.045 ms - 1234
🆘 Xử lý sự cố
<details> <summary>Error: listen EADDRINUSE: address already in use :::5000</summary>Port 5000 đang bị chương trình khác chiếm.
Cách 1 — Tìm và tắt tiến trình đó (Windows):
netstat -ano | findstr :5000
# Ghi lại số PID ở cột cuối, rồi:
taskkill /PID <số-PID> /F
Cách 2 — Đổi sang port khác: sửa bin/www thành '5001'.
Nhớ sửa cả .env.local bên frontend ở bài 12.
Lưu ý macOS: port 5000 bị AirPlay Receiver chiếm. Tắt trong System Settings → General → AirDrop & Handoff → AirPlay Receiver, hoặc dùng port 5001.
</details> <details> <summary>Cannot find module './routes/categories'</summary>Bạn chưa tạo file đó. Comment dòng require tương ứng lại, hoặc làm tiếp bài 05.
Chưa cài nodemon:
npm install --save-dev nodemon
Hoặc chạy trực tiếp: npx nodemon ./bin/www
📝 Bài tập
-
Viết một middleware ghi log thời gian xử lý mỗi request:
[2026-08-16T10:30:00] GET /ping — 5msGợi ý: dùng
Date.now()trước và sau, kết hợp sự kiệnres.on('finish', ...). -
Tạo route
GET /healthtrả về{ status: "ok", uptime: process.uptime() }. -
Thử xóa dòng
app.use(express.json())rồi tạo route:app.post('/test', (req, res) => res.json({ nhanDuoc: req.body }));Gửi POST bằng Postman với body JSON
{"a": 1}. Quan sátreq.bodylà gì. Sau đó thêm lại dòng đó và thử lại. Đây là bài học bạn sẽ nhớ mãi.
All Rights Reserved