Hướng dẫn toàn diện về cách hiểu và triển khai middleware TypeScript trong ứng dụng Express.js. Khám phá các mẫu type nâng cao để có code mạnh mẽ và dễ bảo trì.
Middleware TypeScript: Làm chủ các Mẫu Type cho Middleware trong Express
Express.js, một framework ứng dụng web Node.js tối giản và linh hoạt, cho phép các nhà phát triển xây dựng các API và ứng dụng web mạnh mẽ và có khả năng mở rộng. TypeScript nâng cao Express bằng cách bổ sung kiểu tĩnh, cải thiện khả năng bảo trì code và phát hiện lỗi sớm. Các hàm middleware là nền tảng của Express, cho phép bạn chặn và xử lý các yêu cầu trước khi chúng đến tay các trình xử lý tuyến đường (route handlers) của bạn. Bài viết này khám phá các mẫu type TypeScript nâng cao để định nghĩa và sử dụng middleware trong Express, tăng cường an toàn kiểu dữ liệu và sự rõ ràng của code.
Tìm hiểu về Middleware trong Express
Hàm middleware là các hàm có quyền truy cập vào đối tượng request (req), đối tượng response (res), và hàm middleware tiếp theo trong chu trình yêu cầu-phản hồi của ứng dụng. Các hàm middleware có thể thực hiện các tác vụ sau:
- Thực thi bất kỳ mã lệnh nào.
- Thực hiện thay đổi đối với đối tượng request và response.
- Kết thúc chu trình yêu cầu-phản hồi.
- Gọi hàm middleware tiếp theo trong ngăn xếp (stack).
Các hàm middleware được thực thi tuần tự theo thứ tự chúng được thêm vào ứng dụng Express. Các trường hợp sử dụng phổ biến cho middleware bao gồm:
- Ghi log các yêu cầu.
- Xác thực người dùng.
- Phân quyền truy cập tài nguyên.
- Kiểm tra hợp lệ dữ liệu yêu cầu.
- Xử lý lỗi.
Middleware TypeScript cơ bản
Trong một ứng dụng Express TypeScript cơ bản, một hàm middleware có thể trông như thế này:
import { Request, Response, NextFunction } from 'express';
function loggerMiddleware(req: Request, res: Response, next: NextFunction) {
console.log(`Request: ${req.method} ${req.url}`);
next();
}
export default loggerMiddleware;
Middleware đơn giản này ghi log phương thức và URL của yêu cầu vào console. Hãy cùng phân tích các chú thích kiểu (type annotation):
Request: Đại diện cho đối tượng request của Express.Response: Đại diện cho đối tượng response của Express.NextFunction: Một hàm mà khi được gọi, sẽ thực thi middleware tiếp theo trong ngăn xếp.
Bạn có thể sử dụng middleware này trong ứng dụng Express của mình như sau:
import express from 'express';
import loggerMiddleware from './middleware/loggerMiddleware';
const app = express();
const port = 3000;
app.use(loggerMiddleware);
app.get('/', (req, res) => {
res.send('Hello, world!');
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
Các Mẫu Type Nâng cao cho Middleware
Mặc dù ví dụ middleware cơ bản hoạt động được, nó thiếu sự linh hoạt và an toàn kiểu dữ liệu cho các kịch bản phức tạp hơn. Hãy cùng khám phá các mẫu type nâng cao giúp cải thiện việc phát triển middleware với TypeScript.
1. Tùy chỉnh kiểu Request/Response
Thông thường, bạn sẽ cần mở rộng các đối tượng Request hoặc Response với các thuộc tính tùy chỉnh. Ví dụ, sau khi xác thực, bạn có thể muốn thêm một thuộc tính user vào đối tượng Request. TypeScript cho phép bạn bổ sung các kiểu hiện có bằng cách sử dụng hợp nhất khai báo (declaration merging).
// src/types/express/index.d.ts
import { Request as ExpressRequest } from 'express';
declare global {
namespace Express {
interface Request {
user?: {
id: string;
email: string;
// ... other user properties
};
}
}
}
export {}; // This is needed to make the file a module
Trong ví dụ này, chúng ta đang bổ sung giao diện (interface) Express.Request để bao gồm một thuộc tính user tùy chọn. Bây giờ, trong middleware xác thực của bạn, bạn có thể điền dữ liệu cho thuộc tính này:
import { Request, Response, NextFunction } from 'express';
function authenticationMiddleware(req: Request, res: Response, next: NextFunction) {
// Simulate authentication logic
const userId = req.headers['x-user-id'] as string; // Or fetch from a token, etc.
if (userId) {
// In a real application, you would fetch the user from a database
req.user = {
id: userId,
email: `user${userId}@example.com`
};
next();
} else {
res.status(401).send('Unauthorized');
}
}
export default authenticationMiddleware;
Và trong các trình xử lý tuyến đường của bạn, bạn có thể truy cập thuộc tính req.user một cách an toàn:
import express from 'express';
import authenticationMiddleware from './middleware/authenticationMiddleware';
const app = express();
const port = 3000;
app.use(authenticationMiddleware);
app.get('/profile', (req: Request, res: Response) => {
if (req.user) {
res.send(`Hello, ${req.user.email}! Your user ID is ${req.user.id}`);
} else {
// This should never happen if the middleware is working correctly
res.status(500).send('Internal Server Error');
}
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
2. Middleware Factories (Hàm tạo Middleware)
Middleware factories là các hàm trả về các hàm middleware khác. Mẫu này hữu ích khi bạn cần cấu hình middleware với các tùy chọn hoặc phụ thuộc cụ thể. Ví dụ, hãy xem xét một middleware ghi log ghi các thông báo vào một tệp cụ thể:
import { Request, Response, NextFunction } from 'express';
import fs from 'fs';
import path from 'path';
function createLoggingMiddleware(logFilePath: string) {
return (req: Request, res: Response, next: NextFunction) => {
const logMessage = `[${new Date().toISOString()}] Request: ${req.method} ${req.url}\n`;
fs.appendFile(logFilePath, logMessage, (err) => {
if (err) {
console.error('Error writing to log file:', err);
}
next();
});
};
}
export default createLoggingMiddleware;
Bạn có thể sử dụng hàm tạo middleware này như sau:
import express from 'express';
import createLoggingMiddleware from './middleware/loggingMiddleware';
const app = express();
const port = 3000;
const logFilePath = path.join(__dirname, 'logs', 'requests.log');
app.use(createLoggingMiddleware(logFilePath));
app.get('/', (req, res) => {
res.send('Hello, world!');
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
3. Middleware bất đồng bộ
Các hàm middleware thường cần thực hiện các hoạt động bất đồng bộ, chẳng hạn như truy vấn cơ sở dữ liệu hoặc gọi API. Để xử lý các hoạt động bất đồng bộ một cách chính xác, bạn cần đảm bảo rằng hàm next được gọi sau khi hoạt động bất đồng bộ hoàn tất. Bạn có thể đạt được điều này bằng cách sử dụng async/await hoặc Promises.
import { Request, Response, NextFunction } from 'express';
async function asyncMiddleware(req: Request, res: Response, next: NextFunction) {
try {
// Simulate an asynchronous operation
await new Promise(resolve => setTimeout(resolve, 100));
console.log('Asynchronous operation completed');
next();
} catch (error) {
next(error); // Pass the error to the error handling middleware
}
}
export default asyncMiddleware;
Quan trọng: Hãy nhớ xử lý lỗi trong middleware bất đồng bộ của bạn và chuyển chúng đến middleware xử lý lỗi bằng cách sử dụng next(error). Điều này đảm bảo rằng các lỗi được xử lý và ghi log đúng cách.
4. Middleware Xử lý Lỗi
Middleware xử lý lỗi là một loại middleware đặc biệt dùng để xử lý các lỗi xảy ra trong chu trình yêu cầu-phản hồi. Các hàm middleware xử lý lỗi có bốn đối số: err, req, res, và next.
import { Request, Response, NextFunction } from 'express';
function errorHandler(err: any, req: Request, res: Response, next: NextFunction) {
console.error(err.stack);
res.status(500).send('Something went wrong!');
}
export default errorHandler;
Bạn phải đăng ký middleware xử lý lỗi sau tất cả các middleware và trình xử lý tuyến đường khác. Express xác định middleware xử lý lỗi bằng sự hiện diện của bốn đối số.
import express from 'express';
import asyncMiddleware from './middleware/asyncMiddleware';
import errorHandler from './middleware/errorHandler';
const app = express();
const port = 3000;
app.use(asyncMiddleware);
app.get('/', (req, res) => {
throw new Error('Simulated error!'); // Simulate an error
});
app.use(errorHandler); // Error handling middleware MUST be registered last
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
5. Middleware Kiểm tra hợp lệ Request
Kiểm tra hợp lệ request là một khía cạnh quan trọng của việc xây dựng các API an toàn và đáng tin cậy. Middleware có thể được sử dụng để xác thực dữ liệu yêu cầu đến và đảm bảo rằng nó đáp ứng các tiêu chí nhất định trước khi đến các trình xử lý tuyến đường của bạn. Các thư viện như joi hoặc express-validator có thể được sử dụng để kiểm tra hợp lệ request.
Đây là một ví dụ sử dụng express-validator:
import { Request, Response, NextFunction } from 'express';
import { body, validationResult } from 'express-validator';
const validateCreateUserRequest = [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
(req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
export default validateCreateUserRequest;
Middleware này kiểm tra hợp lệ các trường email và password trong phần thân yêu cầu. Nếu việc kiểm tra không thành công, nó sẽ trả về phản hồi 400 Bad Request kèm theo một mảng các thông báo lỗi. Bạn có thể sử dụng middleware này trong các trình xử lý tuyến đường của mình như sau:
import express from 'express';
import validateCreateUserRequest from './middleware/validateCreateUserRequest';
const app = express();
const port = 3000;
app.post('/users', validateCreateUserRequest, (req, res) => {
// If validation passes, create the user
res.send('User created successfully!');
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
6. Dependency Injection cho Middleware
Khi các hàm middleware của bạn phụ thuộc vào các dịch vụ hoặc cấu hình bên ngoài, dependency injection (tiêm phụ thuộc) có thể giúp cải thiện khả năng kiểm thử và bảo trì. Bạn có thể sử dụng một container dependency injection như tsyringe hoặc đơn giản là truyền các phụ thuộc làm đối số cho các hàm tạo middleware của bạn.
Đây là một ví dụ sử dụng hàm tạo middleware với dependency injection:
// src/services/UserService.ts
export class UserService {
async createUser(email: string, password: string): Promise {
// In a real application, you would save the user to a database
console.log(`Creating user with email: ${email} and password: ${password}`);
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate a database operation
}
}
// src/middleware/createUserMiddleware.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/UserService';
function createCreateUserMiddleware(userService: UserService) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { email, password } = req.body;
await userService.createUser(email, password);
res.status(201).send('User created successfully!');
} catch (error) {
next(error);
}
};
}
export default createCreateUserMiddleware;
// src/app.ts
import express from 'express';
import createCreateUserMiddleware from './middleware/createUserMiddleware';
import { UserService } from './services/UserService';
import errorHandler from './middleware/errorHandler';
const app = express();
const port = 3000;
app.use(express.json()); // Parse JSON request bodies
const userService = new UserService();
const createUserMiddleware = createCreateUserMiddleware(userService);
app.post('/users', createUserMiddleware);
app.use(errorHandler);
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
Các Thực tiễn Tốt nhất cho Middleware TypeScript
- Giữ cho các hàm middleware nhỏ và tập trung. Mỗi hàm middleware nên có một trách nhiệm duy nhất.
- Sử dụng tên mô tả cho các hàm middleware của bạn. Tên nên chỉ rõ chức năng của middleware.
- Xử lý lỗi đúng cách. Luôn bắt lỗi và chuyển chúng đến middleware xử lý lỗi bằng
next(error). - Sử dụng các kiểu request/response tùy chỉnh để tăng cường an toàn kiểu dữ liệu. Bổ sung các giao diện
RequestvàResponsevới các thuộc tính tùy chỉnh khi cần thiết. - Sử dụng các hàm tạo middleware để cấu hình middleware với các tùy chọn cụ thể.
- Tài liệu hóa các hàm middleware của bạn. Giải thích chức năng của middleware và cách sử dụng nó.
- Kiểm thử kỹ lưỡng các hàm middleware của bạn. Viết các bài kiểm thử đơn vị (unit test) để đảm bảo rằng các hàm middleware của bạn hoạt động chính xác.
Kết luận
TypeScript cải thiện đáng kể việc phát triển middleware Express bằng cách bổ sung kiểu tĩnh, cải thiện khả năng bảo trì code và phát hiện lỗi sớm. Bằng cách làm chủ các mẫu type nâng cao như các kiểu request/response tùy chỉnh, hàm tạo middleware, middleware bất đồng bộ, middleware xử lý lỗi, và middleware kiểm tra hợp lệ request, bạn có thể xây dựng các ứng dụng Express mạnh mẽ, có khả năng mở rộng và an toàn về kiểu dữ liệu. Hãy nhớ tuân theo các thực tiễn tốt nhất để giữ cho các hàm middleware của bạn nhỏ, tập trung và được tài liệu hóa tốt.