+1

# 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.

💡 dependencies là thư viện cần khi chạy thật. devDependencies là công cụ chỉ dùng khi phát triển (như nodemon). Dự án gốc để nodemon trong dependencies — 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ự đặt PORT — đâ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ọi res.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.

</details> <details> <summary>nodemon: command not found</summary>

Chưa cài nodemon:

npm install --save-dev nodemon

Hoặc chạy trực tiếp: npx nodemon ./bin/www

</details>

📝 Bài tập

  1. Viết một middleware ghi log thời gian xử lý mỗi request:

    [2026-08-16T10:30:00] GET /ping — 5ms
    

    Gợi ý: dùng Date.now() trước và sau, kết hợp sự kiện res.on('finish', ...).

  2. Tạo route GET /health trả về { status: "ok", uptime: process.uptime() }.

  3. 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át req.body là gì. Sau đó thêm lại dòng đó và thử lại. Đây là bài học bạn sẽ nhớ mãi.


⬅️ Bài trước | Mục lục | Bài tiếp theo: Kết nối Database ➡️


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.