Giới thiệu

Trong môi trường phát triển phần mềm hiện đại, microservice đã trở thành một kiến trúc phổ biến giúp tách biệt các domain logic, tăng khả năng mở rộng và giảm rủi ro khi triển khai. Khi kết hợp với TypeScript, chúng ta không chỉ có được lợi thế của JavaScript mà còn được hỗ trợ mạnh mẽ từ hệ thống kiểu tĩnh, giảm thiểu lỗi runtime và cải thiện năng suất lập trình. Bài viết này sẽ đi sâu vào cách thiết kế, triển khai và vận hành một hệ thống microservice an toàn, hiệu quả bằng TypeScript và Docker, đồng thời chia sẻ các best practices thực tiễn mà các Senior Engineer thường áp dụng.

Kiến trúc microservice với TypeScript

1. Định nghĩa service boundary

Một microservice nên tập trung vào một business capability duy nhất và giao tiếp với các service khác qua giao thức nhẹ như HTTP/REST hoặc gRPC. Khi sử dụng TypeScript, chúng ta có thể mô tả rõ ràng các contract (API contract) bằng các interface hoặc type, giúp cả client và server đồng bộ về cấu trúc dữ liệu.

export interface UserDto {
  id: string;
  name: string;
  age: number;
}

2. Sử dụng DTO và validation

Data Transfer Object (DTO) là lớp trung gian chịu trách nhiệm chuyển đổi và kiểm tra dữ liệu đầu vào. Kết hợp class-validatorclass-transformer giúp chúng ta thực hiện validation một cách declarative, giảm boilerplate code.

import 'reflect-metadata';
import { IsString, IsInt, Min, Max } from 'class-validator';
import { plainToInstance } from 'class-transformer';
import express, { Request, Response } from 'express';
import { validate } from 'class-validator';

class CreateUserDto {
  @IsString()
  name: string;

  @IsInt()
  @Min(0)
  @Max(120)
  age: number;
}

const app = express();
app.use(express.json());

app.post('/users', async (req: Request, res: Response) => {
  const dto = plainToInstance(CreateUserDto, req.body);
  const errors = await validate(dto);
  if (errors.length > 0) {
    return res.status(400).json({ errors });
  }
  // TODO: lưu người dùng vào DB
  res.status(201).json({ message: 'User created' });
});

app.listen(3000, () => console.log('Server running on port 3000'));

3. Xử lý lỗi thống nhất

Một chiến lược quan trọng là tạo một middleware xử lý lỗi chung, trả về cấu trúc JSON thống nhất cho client. Điều này giúp frontend dễ dàng hiển thị thông báo và giảm việc lặp lại mã xử lý lỗi trong từng route.

import { Request, Response, NextFunction } from 'express';

class HttpError extends Error {
  status: number;
  constructor(message: string, status = 500) {
    super(message);
    this.status = status;
  }
}

// Middleware
function errorHandler(err: HttpError, req: Request, res: Response, next: NextFunction) {
  const status = err.status || 500;
  res.status(status).json({
    error: {
      message: err.message,
      status,
    },
  });
}

// Sử dụng trong app
app.use(errorHandler);

Triển khai Docker cho mỗi service

Dockerfile tối ưu cho TypeScript

Đối với dự án TypeScript, chúng ta nên build mã nguồn thành JavaScript trong giai đoạn build, sau đó chỉ copy các file đã biên dịch vào image runtime để giảm kích thước.

# 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   # chạy tsc, output vào /app/dist

# Stage 2: Runtime
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/dist ./dist
RUN npm ci --production
CMD ["node", "dist/index.js"]

Docker Compose cho môi trường đa service

Docker Compose cho phép chúng ta khởi chạy nhiều service đồng thời, định nghĩa mạng nội bộ và biến môi trường một cách rõ ràng.

version: '3.8'
services:
  user-service:
    build: ./services/user
    ports:
      - "3001:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=db
    depends_on:
      - db

  order-service:
    build: ./services/order
    ports:
      - "3002:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=db
    depends_on:
      - db

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=admin
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=appdb
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

CI/CD với GitHub Actions

Automation pipeline giúp chúng ta kiểm tra, build và đẩy Docker image lên registry mỗi khi có commit mới. Dưới đây là một workflow mẫu.

name: CI/CD Microservice

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Node
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Run lint & test
        run: |
          npm run lint
          npm test

      - name: Build Docker image
        run: |
          docker build -t myregistry.com/user-service:${{ github.sha }} ./services/user
          docker push myregistry.com/user-service:${{ github.sha }}

Kiểm thử tự động

Jest kết hợp với ts-jest cho phép viết unit test cho TypeScript mà không cần biên dịch trước. Dưới đây là một ví dụ test cho service tạo người dùng.

import request from 'supertest';
import app from '../src/app';

describe('POST /users', () => {
  it('should create a user with valid data', async () => {
    const res = await request(app)
      .post('/users')
      .send({ name: 'Alice', age: 30 })
      .expect(201);
    expect(res.body.message).toBe('User created');
  });

  it('should reject invalid age', async () => {
    const res = await request(app)
      .post('/users')
      .send({ name: 'Bob', age: -5 })
      .expect(400);
    expect(res.body.errors).toBeDefined();
  });
});

Giám sát và logging

Logging chuẩn JSON giúp các công cụ như ELK stack hoặc Loki dễ dàng thu thập và phân tích. Thư viện pino là lựa chọn nhẹ, tốc độ cao và hỗ trợ streaming.

import pino from 'pino';
const logger = pino({
  level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
  transport: {
    target: 'pino-pretty',
    options: { colorize: true },
  },
});

app.use((req, res, next) => {
  logger.info({ method: req.method, url: req.url });
  next();
});

Kết luận

Việc xây dựng microservice bằng TypeScript và Docker không chỉ mang lại lợi ích về tính ổn định và bảo trì mà còn giúp đội ngũ phát triển giảm thiểu lỗi và tăng tốc độ đưa sản phẩm ra thị trường. Nếu bạn muốn nắm vững các khái niệm cơ bản và nâng cao của TypeScript, Tham khảo khóa học "Lập trình TypeScript từ cơ bản đến nâng cao" tại đây.