Giới thiệu
Trong thời đại các hệ thống phân tán ngày càng phổ biến, việc thiết kế một kiến trúc microservices vừa mạnh mẽ, vừa dễ bảo trì là một thách thức lớn đối với các kỹ sư phần mềm. Bài viết này sẽ đi sâu vào cách xây dựng microservices bằng TypeScript trên nền Node.js, kết hợp với Docker để đạt được tính mở rộng, tính di động và khả năng triển khai tự động (CI/CD). Các khái niệm sẽ được minh họa bằng các ví dụ thực tiễn, giúp bạn nắm bắt được các best practice khi làm việc trong môi trường DevOps hiện đại.
Thiết kế kiến trúc microservices bằng TypeScript
Ưu điểm của TypeScript trong microservices
TypeScript cung cấp hệ thống kiểu tĩnh mạnh mẽ, giúp giảm thiểu lỗi runtime trong các dịch vụ độc lập. Khi mỗi service có một contract rõ ràng (giao diện API), việc sử dụng interface và type để mô tả payload giúp các team phát triển đồng bộ mà không cần phải chạy thử nghiệm liên tục. Ngoài ra, tính năng enum, generics và namespace cho phép tái sử dụng mã nguồn một cách hiệu quả, giảm chi phí bảo trì.
Cấu trúc dự án mẫu
Một dự án microservice tiêu chuẩn thường bao gồm các thư mục sau:
- src: chứa mã nguồn TypeScript.
- src/controllers: các controller xử lý request.
- src/services: business logic.
- src/models: định nghĩa kiểu dữ liệu và entity.
- src/routes: khai báo các route Express.
- src/config: cấu hình môi trường.
- Dockerfile và docker-compose.yml: mô tả cách container hoá.
Dưới đây là một ví dụ về tsconfig.json tối ưu cho môi trường microservice:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Triển khai dịch vụ với Docker
Dockerfile chuẩn cho TypeScript Node.js service
Dockerfile dưới đây sử dụng multi‑stage build để giảm kích thước image cuối cùng. Giai đoạn đầu biên dịch TypeScript, giai đoạn thứ hai chỉ copy các file đã biên dịch và cài đặt runtime dependencies.
# Stage 1: Build FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY tsconfig.json . COPY src ./src RUN npm run build # Stage 2: Runtime FROM node:18-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY package*.json ./ RUN npm ci --only=production EXPOSE 3000 CMD ["node", "dist/index.js"]
Docker Compose cho đa service
Giả sử chúng ta có ba service: auth-service, user-service và gateway. Docker Compose giúp chúng ta khởi chạy đồng thời và cấu hình mạng nội bộ.
version: "3.9"
services:
auth-service:
build: ./auth-service
container_name: auth-service
ports:
- "4000:3000"
environment:
- NODE_ENV=production
- DB_HOST=db
depends_on:
- db
user-service:
build: ./user-service
container_name: user-service
ports:
- "4001:3000"
environment:
- NODE_ENV=production
- DB_HOST=db
depends_on:
- db
gateway:
image: nginx:alpine
container_name: api-gateway
ports:
- "80:80"
volumes:
- ./gateway/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- auth-service
- user-service
db:
image: postgres:15-alpine
container_name: postgres-db
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=appdb
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
Quản lý cấu hình và môi trường
Trong môi trường microservice, việc quản lý cấu hình một cách nhất quán là yếu tố quyết định độ ổn định. Thông thường, chúng ta sử dụng dotenv kết hợp với config để tải các biến môi trường và cung cấp fallback cho các giá trị mặc định.
import * as dotenv from "dotenv";
import * as path from "path";
dotenv.config({ path: path.resolve(__dirname, "../.env") });
export const config = {
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
db: {
host: process.env.DB_HOST || "localhost",
port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
user: process.env.DB_USER || "app",
password: process.env.DB_PASSWORD || "secret",
database: process.env.DB_NAME || "appdb"
},
jwtSecret: process.env.JWT_SECRET || "change_me"
};
Với Docker, các biến môi trường này được truyền qua phần environment trong docker-compose.yml. Khi triển khai trên Kubernetes, bạn có thể chuyển chúng thành ConfigMap và Secret để tăng tính bảo mật.
Giám sát, logging và health check
Đối với mỗi service, việc cung cấp endpoint /health là bắt buộc để orchestrator (Docker Swarm, Kubernetes) có thể kiểm tra trạng thái. Đồng thời, việc ghi log chuẩn (JSON) giúp hệ thống log aggregation (ELK, Loki) dễ dàng phân tích.
import express from "express";
import morgan from "morgan";
const app = express();
// Log dưới dạng JSON
app.use(morgan("json"));
app.get("/health", (req, res) => {
res.status(200).json({ status: "UP", timestamp: new Date().toISOString() });
});
export default app;
Đối với metric, bạn có thể tích hợp prom-client để expose các metric cho Prometheus.
Thực hành: Xây dựng một microservice đơn giản
Bước 1: Khởi tạo dự án
mkdir product-service cd product-service npm init -y npm i express morgan dotenv npm i -D typescript ts-node @types/node @types/express @types/morgan npx tsc --init
Bước 2: Viết controller
// src/controllers/product.controller.ts
import { Request, Response } from "express";
interface Product {
id: number;
name: string;
price: number;
}
const products: Product[] = [
{ id: 1, name: "Laptop", price: 1500 },
{ id: 2, name: "Smartphone", price: 800 }
];
export const getAll = (req: Request, res: Response) => {
res.json(products);
};
export const getById = (req: Request, res: Response) => {
const id = parseInt(req.params.id);
const product = products.find(p => p.id === id);
if (!product) {
return res.status(404).json({ message: "Product not found" });
}
res.json(product);
};
Bước 3: Định nghĩa route
// src/routes/product.routes.ts
import { Router } from "express";
import { getAll, getById } from "../controllers/product.controller";
const router = Router();
router.get("/", getAll);
router.get("/:id", getById);
export default router;
Bước 4: Khởi động server
// src/index.ts
import express from "express";
import productRouter from "./routes/product.routes";
import { config } from "./config";
const app = express();
app.use(express.json());
app.use("/products", productRouter);
app.listen(config.port, () => {
console.log(`Product service listening on port ${config.port}`);
});
Bước 5: Dockerize
Sao chép Dockerfile đã giới thiệu ở phần trên vào thư mục product-service, sau đó chạy:
docker build -t product-service . docker run -d -p 5000:3000 --name product-service product-service
Bây giờ bạn có thể truy cập http://localhost:5000/products để nhận danh sách sản phẩm.
Kết luận
Việc kết hợp TypeScript, Docker và kiến trúc microservices không chỉ giúp tăng tính ổn định và bảo trì mà còn tạo nền tảng vững chắc cho các quy trình DevOps tự động hoá. Nếu bạn muốn nâng cao kiến thức và thực hành sâu hơn, Tham khảo khóa học "Lập trình TypeScript từ cơ bản đến nâng cao" tại đây.







