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 interfacetype để 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, genericsnamespace 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.
  • Dockerfiledocker-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-servicegateway. 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 ConfigMapSecret để 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.