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-validator và class-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.





