Back to blog

Express.js Middleware Deep Dive

expressnodejsbackendjavascriptmiddleware
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 đa

express.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 correlation

Authentication 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 morgan
const 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 - 47

Ghi 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 cors
const 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 helmet
const 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:

HeaderBảo vệ
Content-Security-PolicyXSS, clickjacking
X-DNS-Prefetch-ControlRò rỉ DNS prefetch
X-Frame-OptionsClickjacking
X-XSS-ProtectionXSS filter cũ
X-Content-Type-OptionsMIME sniffing
Strict-Transport-SecurityBuộc dùng HTTPS
Referrer-PolicyRò 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 compression
const 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%)

Parse header Cookie và điền vào req.cookies:

npm install cookie-parser
const 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.json

errors.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-parser

Kiể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

  1. Thêm morgan file logging — Ghi access log ra logs/access.log theo Apache combined format, trong khi giữ dev format trong console.

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

  3. Request correlation — Thêm middleware requestId tạo UUID, gắn vào req và thêm vào response header. Cập nhật logger để bao gồm request ID.

  4. Conditional CORS — Cấu hình CORS cho phép localhost:3000 và localhost:5173 trong development, và chỉ https://myapp.com trong production, dựa trên NODE_ENV.

  5. Graceful error messages — Mở rộng error handler để bắt Mongoose ValidationError và 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

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.