Express.js Middleware Deep Dive

Trong bài trước, bạn đã xây dựng server Express đầu tiên và làm quen với middleware. Bây giờ hãy đi sâu hơn.
Middleware là xương sống của mọi ứng dụng Express. Hiểu cách hoạt động của nó — và cách sử dụng hiệu quả — chính là điều phân biệt code Express cơ bản với code cấp production.
Bạn Sẽ Học Được Gì
Kết Quả Học Tập:
✅ Hiểu chính xác cách middleware hoạt động bên trong
✅ Biết sự khác biệt giữa application-level và router-level middleware
✅ Tạo middleware tùy chỉnh cho logging, authentication và validation
✅ Tích hợp middleware third-party phổ biến (morgan, cors, helmet, compression)
✅ Triển khai xử lý lỗi tập trung với các custom error class
✅ Xử lý async error đúng cách để không crash server
Middleware Hoạt Động Như Thế Nào
Một hàm middleware là bất kỳ hàm nào có signature sau:
function middleware(req, res, next) {
// 1. Làm gì đó (sửa req/res, validate, log, v.v.)
// 2. Gọi next() HOẶC gửi response
next();
}Khi một request đến, Express chạy từng hàm middleware theo thứ tự bạn đăng ký. Mỗi middleware sẽ:
- Gọi
next()để chuyển quyền điều khiển sang middleware/route tiếp theo - Gửi response để kết thúc chu trình (
res.json(),res.send(), v.v.)
Luồng Thực Thi
Nếu bất kỳ middleware nào gửi response, chuỗi dừng lại — các middleware và route handler phía sau bị bỏ qua.
Application-level vs Router-level
Application-level middleware chạy trên mọi request (hoặc một path prefix):
const app = express();
// Chạy với TẤT CẢ requests
app.use(express.json());
// Chỉ chạy với requests đến /api/*
app.use('/api', authMiddleware);Router-level middleware được giới hạn trong một Express Router instance:
const router = express.Router();
// Chỉ áp dụng cho các route của router này
router.use(validateApiKey);
router.get('/users', getUsers);
router.post('/users', createUser);Router-level middleware là chìa khóa để xây dựng ứng dụng mô-đun, dễ bảo trì. Chúng ta sẽ dùng nó nhiều trong bài tiếp theo.
Built-in Middleware
Express 4.x bao gồm một số hàm middleware tích hợp sẵn — không cần cài thêm package.
express.json()
Parse các request có body JSON. Nếu không có nó, req.body sẽ là undefined.
app.use(express.json());
app.post('/todos', (req, res) => {
console.log(req.body); // { title: "Learn Express" }
res.status(201).json({ message: 'Created' });
});Bạn có thể cấu hình giới hạn kích thước:
app.use(express.json({ limit: '10mb' })); // Kích thước body tối đaexpress.urlencoded()
Parse dữ liệu form URL-encoded (application/x-www-form-urlencoded), định dạng mặc định của HTML form.
app.use(express.urlencoded({ extended: true }));
app.post('/contact', (req, res) => {
const { name, email, message } = req.body;
// Xử lý dữ liệu form
});Tùy chọn extended: true cho phép parse các object lồng nhau. Dùng false cho các cặp key-value đơn giản.
express.static()
Phục vụ các file tĩnh (HTML, CSS, ảnh, v.v.) từ một thư mục:
// Phục vụ file từ thư mục "public"
app.use(express.static('public'));
// Với URL prefix
app.use('/assets', express.static('public'));
// Với tùy chọn
app.use(express.static('public', {
maxAge: '1d', // Cache 1 ngày
etag: false, // Tắt ETags
}));express.raw() và express.text()
Cho các trường hợp đặc biệt:
// Parse dữ liệu nhị phân raw (ví dụ: file upload, webhooks)
app.use('/webhooks', express.raw({ type: 'application/json' }));
// Parse body văn bản thuần
app.use(express.text({ type: 'text/plain' }));Custom Middleware
Đây là nơi sức mạnh thực sự của Express thể hiện. Bạn có thể tạo middleware cho bất kỳ cross-cutting concern nào trong ứng dụng.
Request Logging Middleware
Một logger đơn giản ghi lại method, URL và thời gian xử lý:
function requestLogger(req, res, next) {
const start = Date.now();
// Lắng nghe sự kiện response hoàn tất
res.on('finish', () => {
const duration = Date.now() - start;
console.log(
`${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms`
);
});
next();
}
app.use(requestLogger);Dùng res.on('finish') là điều quan trọng — nó cho phép bạn log status code sau khi response thực sự được gửi, không phải trước.
Request ID Middleware
Gán một ID duy nhất cho mỗi request để truy vết:
const { randomUUID } = require('crypto');
function requestId(req, res, next) {
req.id = randomUUID();
res.set('X-Request-ID', req.id);
next();
}
app.use(requestId);
// Bây giờ mỗi req.id là duy nhất — hữu ích cho log correlationAuthentication Middleware
Bảo vệ route bằng cách xác minh token:
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: 'Chưa cung cấp token',
});
}
const token = authHeader.split(' ')[1];
try {
// Trong ứng dụng thực, xác minh JWT ở đây
// const decoded = jwt.verify(token, process.env.JWT_SECRET);
// req.user = decoded;
// Mục đích demo:
if (token !== 'demo-token') {
throw new Error('Token không hợp lệ');
}
req.user = { id: 1, email: '[email protected]' };
next();
} catch (error) {
return res.status(401).json({
success: false,
message: 'Token không hợp lệ hoặc đã hết hạn',
});
}
}
// Áp dụng cho route cụ thể
app.get('/api/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});
// Hoặc áp dụng cho tất cả route dưới một prefix
app.use('/api/protected', authenticate);Request Validation Middleware
Validate các trường bắt buộc trước khi đến route handler:
function validateTodo(req, res, next) {
const { title } = req.body;
const errors = [];
if (!title) {
errors.push('title là bắt buộc');
} else if (typeof title !== 'string') {
errors.push('title phải là chuỗi');
} else if (title.length > 200) {
errors.push('title phải từ 200 ký tự trở xuống');
}
if (errors.length > 0) {
return res.status(400).json({
success: false,
message: 'Validation thất bại',
errors,
});
}
next();
}
app.post('/api/todos', validateTodo, (req, res) => {
// Ở đây chúng ta biết req.body.title hợp lệ
const todo = createTodo(req.body.title);
res.status(201).json({ success: true, data: todo });
});Middleware Factories
Middleware factory là hàm trả về middleware. Hữu ích khi middleware cần được cấu hình:
// Factory: tạo middleware kiểm tra role cụ thể
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: 'Chưa đăng nhập' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
message: `Yêu cầu một trong các role: ${roles.join(', ')}`,
});
}
next();
};
}
// Sử dụng
app.delete('/api/users/:id', authenticate, requireRole('admin'), deleteUser);
app.get('/api/reports', authenticate, requireRole('admin', 'manager'), getReports);// Factory: rate limiter cho từng route
function rateLimit(maxRequests, windowMs) {
const requests = new Map();
return (req, res, next) => {
const key = req.ip;
const now = Date.now();
const windowStart = now - windowMs;
// Dọn dẹp entries cũ
const userRequests = (requests.get(key) || []).filter(
time => time > windowStart
);
if (userRequests.length >= maxRequests) {
return res.status(429).json({
success: false,
message: 'Quá nhiều request. Vui lòng thử lại sau.',
});
}
userRequests.push(now);
requests.set(key, userRequests);
next();
};
}
// 5 request mỗi phút cho login
app.post('/api/auth/login', rateLimit(5, 60 * 1000), loginHandler);
// 100 request mỗi phút cho API chung
app.use('/api', rateLimit(100, 60 * 1000));Third-party Middleware
Các package này xử lý các mối quan tâm production phổ biến, đã được kiểm thử kỹ lưỡng và sẵn sàng sử dụng.
morgan — HTTP Request Logger
Morgan ghi log HTTP request theo định dạng chuẩn. Mạnh hơn nhiều so với logger tự viết.
npm install morganconst morgan = require('morgan');
// Các format có sẵn
app.use(morgan('dev')); // Có màu, ngắn gọn (tuyệt vời cho dev)
app.use(morgan('combined')); // Apache combined log (tuyệt vời cho prod)
app.use(morgan('short')); // Ngắn hơn combined
app.use(morgan('tiny')); // Output tối giản
// Format tùy chỉnh
app.use(morgan(':method :url :status :response-time ms - :res[content-length]'));Output của morgan('dev'):
GET /api/todos 200 4.823 ms - 245
POST /api/todos 201 2.156 ms - 89
GET /api/todos/999 404 1.203 ms - 47Ghi log ra file trong production:
const fs = require('fs');
const path = require('path');
// Tạo write stream
const accessLogStream = fs.createWriteStream(
path.join(__dirname, 'logs', 'access.log'),
{ flags: 'a' } // Chế độ append
);
app.use(morgan('combined', { stream: accessLogStream }));cors — Cross-Origin Resource Sharing
Kiểm soát domain nào có thể truy cập API từ trình duyệt.
npm install corsconst cors = require('cors');
// Cho phép TẤT CẢ origins (không an toàn cho production API có dữ liệu nhạy cảm)
app.use(cors());
// Chỉ cho phép origin cụ thể
app.use(cors({
origin: 'https://myapp.com',
}));
// Cho phép nhiều origin
app.use(cors({
origin: ['https://myapp.com', 'https://admin.myapp.com'],
}));
// Kiểm tra origin động
app.use(cors({
origin: (origin, callback) => {
const allowedOrigins = ['https://myapp.com', 'https://staging.myapp.com'];
// Cho phép request không có origin (ví dụ: app mobile, curl)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Không được phép bởi CORS'));
}
},
credentials: true, // Cho phép cookies / Authorization headers
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));Áp dụng CORS theo từng route để kiểm soát chi tiết:
// Endpoint công khai
app.get('/api/public', cors(), getPublicData);
// Endpoint hạn chế
app.get('/api/admin', cors({ origin: 'https://admin.myapp.com' }), getAdminData);helmet — Security Headers
Thiết lập HTTP security headers để bảo vệ khỏi các lỗ hổng web phổ biến. Dùng trong mọi ứng dụng Express production.
npm install helmetconst helmet = require('helmet');
// Bật tất cả mặc định (khuyến nghị)
app.use(helmet());helmet() thiết lập các header sau theo mặc định:
| Header | Bảo vệ |
|---|---|
Content-Security-Policy | XSS, clickjacking |
X-DNS-Prefetch-Control | Rò rỉ DNS prefetch |
X-Frame-Options | Clickjacking |
X-XSS-Protection | XSS filter cũ |
X-Content-Type-Options | MIME sniffing |
Strict-Transport-Security | Buộc dùng HTTPS |
Referrer-Policy | Rò rỉ Referrer |
Tùy chỉnh từng bảo vệ:
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'cdn.jsdelivr.net'],
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
crossOriginEmbedderPolicy: false, // Tắt nếu nhúng nội dung
}));compression — Response Compression
Nén gzip/Brotli responses, giảm đáng kể kích thước payload.
npm install compressionconst compression = require('compression');
// Nén tất cả responses
app.use(compression());
// Chỉ nén responses lớn hơn 1KB
app.use(compression({ threshold: 1024 }));
// Filter tùy chỉnh — không nén một số content type
app.use(compression({
filter: (req, res) => {
// Không nén nếu client chủ động từ chối
if (req.headers['x-no-compression']) return false;
// Dùng filter mặc định trong các trường hợp khác
return compression.filter(req, res);
},
}));So sánh kích thước response (payload JSON điển hình):
- Không nén: ~50KB
- Với gzip: ~8KB (nhỏ hơn 84%)
cookie-parser — Parse Cookies
Parse header Cookie và điền vào req.cookies:
npm install cookie-parserconst cookieParser = require('cookie-parser');
app.use(cookieParser());
// Tùy chọn, ký cookie với một secret
app.use(cookieParser('my-secret-key'));
// Đọc cookie
app.get('/profile', (req, res) => {
console.log(req.cookies.sessionId); // Cookie chưa ký
console.log(req.signedCookies.userId); // Cookie đã ký
});
// Thiết lập cookie
app.post('/login', (req, res) => {
res.cookie('sessionId', 'abc123', {
httpOnly: true, // Không truy cập được qua JS
secure: true, // Chỉ HTTPS
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 ngày
sameSite: 'strict',
});
res.json({ message: 'Đã đăng nhập' });
});Error Handling Middleware
Xử lý lỗi là phần quan trọng nhất và hay bị hiểu nhầm nhất của Express middleware. Hãy làm đúng.
Signature của Error Handler
Middleware xử lý lỗi có bốn tham số — tham số err thêm vào là cách Express biết đó là error handler:
// Middleware thông thường: 3 params
function middleware(req, res, next) { ... }
// Error handler: 4 params (err PHẢI là đầu tiên)
function errorHandler(err, req, res, next) { ... }Quy tắc quan trọng: Luôn đăng ký error-handling middleware SAU CÙNG, sau tất cả routes và middleware khác.
Kích Hoạt Error Handler
Truyền lỗi vào next() để bỏ qua tất cả middleware thông thường còn lại và nhảy thẳng đến error handler:
app.get('/users/:id', (req, res, next) => {
try {
const user = getUserById(req.params.id);
if (!user) {
// Truyền lỗi vào next()
const err = new Error('Không tìm thấy người dùng');
err.status = 404;
return next(err);
}
res.json(user);
} catch (error) {
next(error); // Chuyển lỗi không mong đợi đến error handler
}
});Custom Error Classes
Các custom error class giúp dễ dàng phân biệt operational error (input sai, không tìm thấy) với programming error (bug):
// errors.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.isOperational = true; // Lỗi có thể dự đoán, hiển thị cho người dùng
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} không tìm thấy`, 404);
}
}
class ValidationError extends AppError {
constructor(message, errors = []) {
super(message, 400);
this.errors = errors;
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Yêu cầu xác thực') {
super(message, 401);
}
}
class ForbiddenError extends AppError {
constructor(message = 'Truy cập bị từ chối') {
super(message, 403);
}
}
module.exports = { AppError, NotFoundError, ValidationError, UnauthorizedError, ForbiddenError };Centralized Error Handler
Một error handler để xử lý tất cả:
const { AppError } = require('./errors');
function errorHandler(err, req, res, next) {
// Mặc định 500 nếu không có status code
const statusCode = err.statusCode || err.status || 500;
const isProduction = process.env.NODE_ENV === 'production';
// Log lỗi (luôn luôn)
if (statusCode >= 500) {
console.error('💥 Lỗi server:', {
message: err.message,
stack: err.stack,
url: req.originalUrl,
method: req.method,
});
}
// Operational errors: an toàn để hiển thị cho client
if (err.isOperational) {
return res.status(statusCode).json({
success: false,
message: err.message,
...(err.errors && { errors: err.errors }), // Bao gồm lỗi từng trường nếu có
});
}
// Lỗi programming/không mong đợi: ẩn chi tiết trong production
res.status(500).json({
success: false,
message: isProduction ? 'Đã xảy ra lỗi' : err.message,
...(isProduction ? {} : { stack: err.stack }),
});
}
// Đăng ký SAU CÙNG
app.use(errorHandler);Sử dụng custom errors trong routes:
const { NotFoundError, ValidationError, ForbiddenError } = require('./errors');
app.get('/api/todos/:id', (req, res, next) => {
try {
const todo = todos.find(t => t.id === parseInt(req.params.id));
if (!todo) throw new NotFoundError('Todo');
res.json({ success: true, data: todo });
} catch (error) {
next(error);
}
});
app.post('/api/todos', (req, res, next) => {
try {
const { title } = req.body;
if (!title) {
throw new ValidationError('Validation thất bại', ['title là bắt buộc']);
}
const todo = { id: Date.now(), title, completed: false };
res.status(201).json({ success: true, data: todo });
} catch (error) {
next(error);
}
});Xử Lý Async Error
Express 4.x không tự động bắt lỗi ném ra trong async functions. Bạn phải tự bắt:
// ❌ Sai — unhandled promise rejection khiến server crash
app.get('/api/users', async (req, res) => {
const users = await User.findAll(); // Nếu throw lỗi, Express sẽ không bắt được
res.json(users);
});
// ✅ Tùy chọn 1 — try/catch trong mỗi async handler
app.get('/api/users', async (req, res, next) => {
try {
const users = await User.findAll();
res.json(users);
} catch (error) {
next(error); // Chuyển đến error handler
}
});
// ✅ Tùy chọn 2 — asyncHandler wrapper (loại bỏ code lặp)
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get('/api/users', asyncHandler(async (req, res) => {
const users = await User.findAll();
res.json(users);
}));
app.post('/api/todos', asyncHandler(async (req, res) => {
const todo = await Todo.create(req.body);
res.status(201).json({ success: true, data: todo });
}));Pattern asyncHandler wrapper đơn giản nhưng mạnh mẽ. Nhiều team trích xuất nó vào file utilities dùng chung và sử dụng khắp nơi.
Lưu ý: Express 5 (hiện đang beta) tự động xử lý async error, loại bỏ nhu cầu dùng
asyncHandler. Với Express 4, wrapper là giải pháp gọn nhất.
Xử Lý 404
Handler "not found" bắt các request đến route không tồn tại. Đặt sau tất cả routes nhưng trước error handler:
// Tất cả routes định nghĩa ở trên...
// 404 — không có route nào khớp
app.use((req, res, next) => {
next(new NotFoundError(`Route ${req.method} ${req.originalUrl}`));
});
// Error handler — phải là cuối cùng
app.use(errorHandler);Dự Án Thực Hành: Nâng Cấp Todo API
Hãy nâng cấp Todo API từ Bài 1 với middleware cấp production.
Cấu Trúc Dự Án
express-todo-api/
├── index.js
├── errors.js # Custom error classes
├── middleware/
│ ├── auth.js # Authentication
│ ├── logger.js # Request logging
│ └── validate.js # Input validation
└── package.jsonerrors.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} không tìm thấy`, 404);
}
}
class ValidationError extends AppError {
constructor(message, errors = []) {
super(message, 400);
this.errors = errors;
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Yêu cầu xác thực') {
super(message, 401);
}
}
module.exports = { AppError, NotFoundError, ValidationError, UnauthorizedError };middleware/logger.js
function requestLogger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const color =
res.statusCode >= 500 ? '\x1b[31m' : // Đỏ
res.statusCode >= 400 ? '\x1b[33m' : // Vàng
res.statusCode >= 300 ? '\x1b[36m' : // Cyan
'\x1b[32m'; // Xanh lá
const reset = '\x1b[0m';
console.log(
`${color}${req.method}${reset} ${req.originalUrl} ` +
`${color}${res.statusCode}${reset} ${duration}ms`
);
});
next();
}
module.exports = requestLogger;middleware/auth.js
const { UnauthorizedError } = require('../errors');
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return next(new UnauthorizedError('Chưa cung cấp token'));
}
const token = authHeader.split(' ')[1];
try {
// Thay bằng xác minh JWT thực:
// const decoded = jwt.verify(token, process.env.JWT_SECRET);
// req.user = decoded;
if (token !== 'demo-token') throw new Error('Token không hợp lệ');
req.user = { id: 1, email: '[email protected]' };
next();
} catch {
next(new UnauthorizedError('Token không hợp lệ hoặc đã hết hạn'));
}
}
module.exports = authenticate;middleware/validate.js
const { ValidationError } = require('../errors');
function validateTodo(req, res, next) {
const { title } = req.body;
const errors = [];
if (!title) {
errors.push('title là bắt buộc');
} else if (typeof title !== 'string') {
errors.push('title phải là chuỗi');
} else if (title.trim().length === 0) {
errors.push('title không được để trống');
} else if (title.length > 200) {
errors.push('title phải từ 200 ký tự trở xuống');
}
if (errors.length > 0) {
return next(new ValidationError('Validation thất bại', errors));
}
// Sanitize
req.body.title = req.body.title.trim();
next();
}
module.exports = { validateTodo };index.js — Ứng Dụng Đã Nâng Cấp Hoàn Chỉnh
const express = require('express');
const morgan = require('morgan');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const requestLogger = require('./middleware/logger');
const authenticate = require('./middleware/auth');
const { validateTodo } = require('./middleware/validate');
const { AppError, NotFoundError } = require('./errors');
const app = express();
const PORT = process.env.PORT || 3000;
// ─── Bảo mật & Hiệu suất ────────────────────────────────────────────────
app.use(helmet());
app.use(compression());
// ─── CORS ───────────────────────────────────────────────────────────────
app.use(cors({
origin: process.env.ALLOWED_ORIGIN || '*',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
// ─── Phân tích Body ──────────────────────────────────────────────────────
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true }));
// ─── Logging ─────────────────────────────────────────────────────────────
app.use(morgan('dev')); // HTTP request logging
app.use(requestLogger); // Custom timing logger
// ─── In-memory Database ─────────────────────────────────────────────────
let todos = [
{ id: 1, title: 'Học Express.js', completed: false, createdAt: new Date() },
{ id: 2, title: 'Thành thạo Middleware', completed: false, createdAt: new Date() },
];
let nextId = 3;
// ─── Public Routes ───────────────────────────────────────────────────────
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date() });
});
// ─── Protected Routes ────────────────────────────────────────────────────
// GET /api/todos
app.get('/api/todos', authenticate, (req, res) => {
const { completed } = req.query;
let result = todos;
if (completed !== undefined) {
result = todos.filter(t => t.completed === (completed === 'true'));
}
res.json({ success: true, data: result, total: result.length });
});
// GET /api/todos/:id
app.get('/api/todos/:id', authenticate, (req, res, next) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return next(new AppError('Định dạng ID không hợp lệ', 400));
}
const todo = todos.find(t => t.id === id);
if (!todo) return next(new NotFoundError('Todo'));
res.json({ success: true, data: todo });
});
// POST /api/todos
app.post('/api/todos', authenticate, validateTodo, (req, res) => {
const todo = {
id: nextId++,
title: req.body.title,
completed: false,
createdAt: new Date(),
};
todos.push(todo);
res.status(201).json({ success: true, data: todo });
});
// PUT /api/todos/:id
app.put('/api/todos/:id', authenticate, (req, res, next) => {
const id = parseInt(req.params.id);
const index = todos.findIndex(t => t.id === id);
if (index === -1) return next(new NotFoundError('Todo'));
const { title, completed } = req.body;
if (title !== undefined) todos[index].title = title.trim();
if (completed !== undefined) todos[index].completed = Boolean(completed);
res.json({ success: true, data: todos[index] });
});
// DELETE /api/todos/:id
app.delete('/api/todos/:id', authenticate, (req, res, next) => {
const id = parseInt(req.params.id);
const index = todos.findIndex(t => t.id === id);
if (index === -1) return next(new NotFoundError('Todo'));
const [deleted] = todos.splice(index, 1);
res.json({ success: true, data: deleted });
});
// ─── 404 Handler ─────────────────────────────────────────────────────────
app.use((req, res, next) => {
next(new NotFoundError(`Route ${req.method} ${req.originalUrl}`));
});
// ─── Error Handler (luôn ở cuối cùng) ────────────────────────────────────
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const isProduction = process.env.NODE_ENV === 'production';
if (statusCode >= 500) {
console.error('💥 Lỗi:', err.message, '\n', err.stack);
}
if (err.isOperational) {
return res.status(statusCode).json({
success: false,
message: err.message,
...(err.errors && { errors: err.errors }),
});
}
res.status(500).json({
success: false,
message: isProduction ? 'Lỗi server nội bộ' : err.message,
});
});
app.listen(PORT, () => {
console.log(`Server đang chạy tại http://localhost:${PORT}`);
});Cài đặt dependencies:
npm install morgan cors helmet compression cookie-parserKiểm Thử API Đã Nâng Cấp
# Health check (không cần auth)
curl http://localhost:3000/health
# Lấy todos (cần auth)
curl http://localhost:3000/api/todos \
-H "Authorization: Bearer demo-token"
# Tạo todo
curl -X POST http://localhost:3000/api/todos \
-H "Authorization: Bearer demo-token" \
-H "Content-Type: application/json" \
-d '{"title":"Thành thạo middleware"}'
# Lọc theo completed
curl "http://localhost:3000/api/todos?completed=false" \
-H "Authorization: Bearer demo-token"
# Kích hoạt 401
curl http://localhost:3000/api/todos
# Kích hoạt lỗi validation 400
curl -X POST http://localhost:3000/api/todos \
-H "Authorization: Bearer demo-token" \
-H "Content-Type: application/json" \
-d '{}'
# Kích hoạt 404
curl http://localhost:3000/api/todos/9999 \
-H "Authorization: Bearer demo-token"Thứ Tự Middleware: Các Quy Tắc Vàng
Sắp xếp middleware sai là một trong những lỗi Express phổ biến nhất. Đây là quy tắc:
Tại sao thứ tự này quan trọng:
- Security headers phải được thiết lập trên mọi response, kể cả error response — vì vậy đặt đầu tiên
- Compression phải bao gồm toàn bộ response — đặt gần đầu
- Body parsers phải chạy trước bất kỳ route hay middleware nào đọc
req.body - Logging phải ghi lại mọi request, kể cả những request gặp lỗi auth hay route
- Auth phải chạy trước các route cần dữ liệu người dùng
- Error handler phải ở cuối cùng — nó xử lý lỗi từ tất cả mọi thứ ở trên
Tóm Tắt và Những Điều Cần Nhớ
Những Gì Chúng Ta Đã Học:
✅ Middleware là các hàm với (req, res, next) tạo thành pipeline xử lý
✅ Application-level middleware áp dụng toàn cục; router-level middleware có phạm vi giới hạn
✅ Built-in middleware: express.json(), express.urlencoded(), express.static()
✅ Custom middleware cho logging, auth, validation và rate limiting
✅ Middleware factory trả về middleware đã cấu hình — tuyệt vời cho logic có thể tái sử dụng và tham số hóa
✅ Third-party: morgan cho logging, cors cho CORS, helmet cho security headers, compression cho gzip
✅ Error handler có 4 params (err, req, res, next) và phải đăng ký sau cùng
✅ Dùng custom error class để phân biệt operational error với bug
✅ Bọc async handler với asyncHandler hoặc try/catch để tránh unhandled promise rejection
Quy Tắc Vàng: Thứ tự middleware quan trọng. Bảo mật → Phân tích → Logging → Auth → Routes → 404 → Error Handler.
Bài Tập Thực Hành
-
Thêm morgan file logging — Ghi access log ra
logs/access.logtheo Apache combined format, trong khi giữ dev format trong console. -
IP-based rate limiter — Xây dựng middleware factory giới hạn request theo IP với cửa sổ thời gian và số request tối đa có thể cấu hình. Dùng
Mapđể theo dõi timestamp của request. -
Request correlation — Thêm middleware
requestIdtạo UUID, gắn vàoreqvà thêm vào response header. Cập nhật logger để bao gồm request ID. -
Conditional CORS — Cấu hình CORS cho phép
localhost:3000vàlocalhost:5173trong development, và chỉhttps://myapp.comtrong production, dựa trênNODE_ENV. -
Graceful error messages — Mở rộng error handler để bắt Mongoose
ValidationErrorvàCastError(hoặc mô phỏng chúng) và trả về response 400/404 gọn gàng thay vì để lộ thông tin nội bộ của Mongoose.
Tiếp Theo
Trong bài tiếp theo, chúng ta sẽ đi sâu vào Routing và Request Handling:
- Tổ chức routes với Express Router
- Cấu trúc route lồng nhau và mô-đun
- Validate request với
express-validator - Xử lý file upload với
multer - Best practices thiết kế RESTful API
Các pattern chúng ta đã học ở đây — đặc biệt là error handling và middleware factory — sẽ được sử dụng xuyên suốt phần còn lại của series.
Tài Nguyên Bổ Sung
- Hướng dẫn Middleware Express.js
- Xử lý lỗi Express
- Tài liệu morgan
- Tài liệu helmet
- Tài liệu cors
- Tài liệu compression
Tiếp Theo Trong Series:
- Bài 3: Routing và Request Handling
- Bài 4: Làm việc với MongoDB và Mongoose
- Bài 5: Làm việc với PostgreSQL và Prisma
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.