Back to blog

Template Cho Dự Án Nhỏ: Một Stack Full TypeScript

TypeScriptMonorepoExpressReactPulumiAzureDevOpsPostgreSQL
Template Cho Dự Án Nhỏ: Một Stack Full TypeScript

Trong công ty của tôi, chúng tôi có nhiều dự án nhỏ thường bao gồm một vài trang với logic nghiệp vụ đơn giản. Mỗi lần bắt đầu phát triển một dự án như thế này, chúng tôi dành thời gian cho việc thiết lập dự án với boilerplate code cho các tech stack khác nhau. Khi chúng tôi giao dự án cho các nhóm khác nhau, họ có thể sử dụng các công nghệ khác nhau hoặc thiết lập môi trường phát triển khác nhau. Nhiệm vụ lặp đi lặp lại này tiêu tốn thời gian quý báu có thể dành cho phát triển thực tế.

Chúng Ta Cần Một Template Cho Dự Án Nhỏ

Để giảm thời gian dành cho việc thiết lập dự án lặp lại, chúng tôi muốn một template bootstrap mọi thứ bằng một lệnh duy nhất. Bằng cách làm điều này, chúng tôi có thể tiết kiệm thời gian đáng kể và tăng tốc giai đoạn phát triển, cho phép chúng tôi nhanh chóng chuyển sang các dự án khác.

Sau khi đánh giá nhu cầu của mình, chúng tôi quyết định sử dụng một stack full TypeScript. Tại sao? Vì:

  • Một ngôn ngữ duy nhất trên toàn bộ stack - Ít chuyển đổi ngữ cảnh cho nhà phát triển
  • Type safety ở mọi nơi - Từ frontend đến backend đến infrastructure
  • Chia sẻ type giữa các service - Không còn sai lệch hợp đồng API
  • Hỗ trợ IDE tốt hơn - IntelliSense, refactoring và phát hiện lỗi trên toàn dự án

Tech Stack

Đây là những gì template của chúng tôi bao gồm:

Backend: Express 5 + TypeScript

Express 5 mang lại hỗ trợ Promise native, điều này có nghĩa là xử lý async/await tốt hơn mà không cần thư viện wrapper. Kết hợp với TypeScript, chúng ta có:

// src/server/routes/users.ts
import { Router, Request, Response } from 'express'
import { UserService } from '../services/user.service'
import { CreateUserDto, UserResponse } from '@shared/types'
 
const router = Router()
 
router.post('/users', async (req: Request<{}, UserResponse, CreateUserDto>, res: Response<UserResponse>) => {
  const user = await UserService.create(req.body)
  res.json(user)
})
 
export default router

Cấu hình chính trong tsconfig.json cho backend:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "paths": {
      "@shared/*": ["../shared/*"]
    }
  }
}

Frontend: React 19 + TypeScript + ShadCN UI + React Router

React 19 giới thiệu các tính năng thú vị như Server Components và cải thiện concurrent rendering. Kết hợp với ShadCN UI, chúng ta có các component đẹp, dễ tiếp cận mà không bị phình to như các thư viện component truyền thống.

// src/client/pages/Dashboard.tsx
import { useQuery } from '@tanstack/react-query'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { UserList } from '@/components/UserList'
import type { User } from '@shared/types'
 
export function Dashboard() {
  const { data: users, isLoading } = useQuery<User[]>({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => res.json())
  })
 
  if (isLoading) return <div>Loading...</div>
 
  return (
    <Card>
      <CardHeader>
        <CardTitle>Users</CardTitle>
      </CardHeader>
      <CardContent>
        <UserList users={users ?? []} />
        <Button variant="outline">Add User</Button>
      </CardContent>
    </Card>
  )
}

React Router xử lý điều hướng với các tham số route type-safe:

// src/client/routes.tsx
import { createBrowserRouter } from 'react-router-dom'
import { Dashboard } from './pages/Dashboard'
import { UserDetail } from './pages/UserDetail'
 
export const router = createBrowserRouter([
  { path: '/', element: <Dashboard /> },
  { path: '/users/:id', element: <UserDetail /> }
])

Database: PostgreSQL

Đối với các dự án nhỏ, chúng tôi triển khai PostgreSQL trên cùng một VM với ứng dụng. Điều này giữ cho cơ sở hạ tầng đơn giản và hiệu quả về chi phí. Để phát triển local, chúng tôi sử dụng Docker Compose để khởi động một instance PostgreSQL.

Chúng tôi sử dụng Drizzle ORM cho các thao tác database type-safe:

// packages/server/src/db/schema.ts
import { pgTable, uuid, varchar, timestamp } from 'drizzle-orm/pg-core'
 
export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 255 }).notNull(),
  passwordHash: varchar('password_hash', { length: 255 }).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull()
})
 
export type User = typeof users.$inferSelect
export type NewUser = typeof users.$inferInsert
// packages/server/src/db/index.ts
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from './schema'
 
const pool = new Pool({
  connectionString: process.env.DATABASE_URL
})
 
export const db = drizzle(pool, { schema })

Cấu hình database trong package server:

// packages/server/drizzle.config.ts
import type { Config } from 'drizzle-kit'
 
export default {
  schema: './src/db/schema.ts',
  out: './src/db/migrations',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!
  }
} satisfies Config

Infrastructure: Pulumi + TypeScript + Azure VM

Đây là nơi TypeScript thực sự tỏa sáng. Thay vì viết YAML hoặc HCL, chúng ta định nghĩa cơ sở hạ tầng với code thực:

// packages/infra/src/index.ts
import * as pulumi from '@pulumi/pulumi'
import * as azure from '@pulumi/azure-native'
import { createVmSetupScript } from './vm-setup'
 
const config = new pulumi.Config()
const environment = pulumi.getStack()
 
// Resource Group
const resourceGroup = new azure.resources.ResourceGroup('rg', {
  resourceGroupName: `rg-myapp-${environment}`,
  location: 'Southeast Asia'
})
 
// Virtual Network
const vnet = new azure.network.VirtualNetwork('vnet', {
  resourceGroupName: resourceGroup.name,
  virtualNetworkName: `vnet-myapp-${environment}`,
  addressSpace: { addressPrefixes: ['10.0.0.0/16'] },
  subnets: [{
    name: 'default',
    addressPrefix: '10.0.1.0/24'
  }]
})
 
// Network Security Group - Allow SSH, HTTP, HTTPS
const nsg = new azure.network.NetworkSecurityGroup('nsg', {
  resourceGroupName: resourceGroup.name,
  securityRules: [
    { name: 'SSH', priority: 1000, access: 'Allow', direction: 'Inbound',
      protocol: 'Tcp', sourcePortRange: '*', destinationPortRange: '22',
      sourceAddressPrefix: '*', destinationAddressPrefix: '*' },
    { name: 'HTTP', priority: 1001, access: 'Allow', direction: 'Inbound',
      protocol: 'Tcp', sourcePortRange: '*', destinationPortRange: '80',
      sourceAddressPrefix: '*', destinationAddressPrefix: '*' },
    { name: 'HTTPS', priority: 1002, access: 'Allow', direction: 'Inbound',
      protocol: 'Tcp', sourcePortRange: '*', destinationPortRange: '443',
      sourceAddressPrefix: '*', destinationAddressPrefix: '*' }
  ]
})
 
// Public IP
const publicIp = new azure.network.PublicIPAddress('pip', {
  resourceGroupName: resourceGroup.name,
  publicIPAllocationMethod: 'Static',
  sku: { name: 'Standard' }
})
 
// Network Interface
const nic = new azure.network.NetworkInterface('nic', {
  resourceGroupName: resourceGroup.name,
  ipConfigurations: [{
    name: 'ipconfig',
    subnet: { id: vnet.subnets.apply(s => s![0].id!) },
    publicIPAddress: { id: publicIp.id }
  }],
  networkSecurityGroup: { id: nsg.id }
})
 
// VM Setup Script - installs Node.js, PostgreSQL, Nginx, PM2
const vmSetupScript = createVmSetupScript({
  dbName: config.require('dbName'),
  dbUser: config.require('dbUser'),
  dbPassword: config.requireSecret('dbPassword')
})
 
// Virtual Machine
const vm = new azure.compute.VirtualMachine('vm', {
  resourceGroupName: resourceGroup.name,
  vmName: `vm-myapp-${environment}`,
  vmSize: config.get('vmSize') || 'Standard_B2s',
  osProfile: {
    computerName: 'myapp',
    adminUsername: config.require('adminUsername'),
    adminPassword: config.requireSecret('adminPassword'),
    customData: vmSetupScript  // Base64-encoded cloud-init script
  },
  storageProfile: {
    imageReference: {
      publisher: 'Canonical',
      offer: '0001-com-ubuntu-server-jammy',
      sku: '22_04-lts-gen2',
      version: 'latest'
    },
    osDisk: {
      createOption: 'FromImage',
      managedDisk: { storageAccountType: 'Standard_LRS' },
      diskSizeGB: 30
    }
  },
  networkProfile: {
    networkInterfaces: [{ id: nic.id, primary: true }]
  }
})
 
export const vmPublicIp = publicIp.ipAddress
export const resourceGroupName = resourceGroup.name
export const vmName = vm.name

VM setup script cài đặt tất cả phần mềm cần thiết bao gồm PostgreSQL:

// packages/infra/src/vm-setup.ts
import * as pulumi from '@pulumi/pulumi'
 
interface VmSetupConfig {
  dbName: string
  dbUser: string
  dbPassword: pulumi.Output<string>
}
 
export function createVmSetupScript(config: VmSetupConfig): pulumi.Output<string> {
  return config.dbPassword.apply(dbPassword => {
    const script = `#!/bin/bash
set -e
 
# Update system
apt-get update && apt-get upgrade -y
 
# Install Node.js 20
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
 
# Install PostgreSQL 16
sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
apt-get update
apt-get install -y postgresql-16
 
# Configure PostgreSQL
sudo -u postgres psql -c "CREATE USER ${config.dbUser} WITH PASSWORD '${dbPassword}';"
sudo -u postgres psql -c "CREATE DATABASE ${config.dbName} OWNER ${config.dbUser};"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${config.dbName} TO ${config.dbUser};"
 
# Allow local connections
echo "host all all 127.0.0.1/32 md5" >> /etc/postgresql/16/main/pg_hba.conf
systemctl restart postgresql
 
# Install Nginx
apt-get install -y nginx
 
# Configure Nginx as reverse proxy
cat > /etc/nginx/sites-available/myapp << 'NGINX'
server {
    listen 80;
    server_name _;
 
    # Frontend (static files)
    location / {
        root /app/client;
        try_files $uri $uri/ /index.html;
    }
 
    # Backend API
    location /api {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_cache_bypass $http_upgrade;
    }
}
NGINX
 
ln -sf /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
systemctl restart nginx
 
# Install PM2
npm install -g pm2
 
# Create app directory
mkdir -p /app/{client,server}
chown -R deploy:deploy /app
 
# Create deploy user
useradd -m -s /bin/bash deploy
echo "deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx" >> /etc/sudoers.d/deploy
 
echo "VM setup complete!"
`
    return Buffer.from(script).toString('base64')
  })
}

Lợi ích của infrastructure-as-code với TypeScript:

  • Hỗ trợ refactoring - Đổi tên tài nguyên một cách an toàn trên toàn codebase
  • Type checking - Phát hiện lỗi cấu hình trước khi triển khai
  • Component tái sử dụng - Tạo các factory function cho các pattern phổ biến
  • Testing - Viết unit test cho cơ sở hạ tầng của bạn

Cấu Trúc Monorepo

Chúng tôi sử dụng cấu trúc monorepo để giữ mọi thứ cùng nhau trong khi duy trì sự tách biệt concerns:

project-root/
├── packages/
│   ├── client/              # React frontend
│   │   ├── src/
│   │   ├── package.json
│   │   ├── vite.config.ts
│   │   ├── vitest.config.ts
│   │   └── tsconfig.json
│   ├── server/              # Express backend
│   │   ├── src/
│   │   │   └── db/
│   │   │       ├── schema.ts
│   │   │       ├── migrations/
│   │   │       └── seeds/
│   │   ├── package.json
│   │   ├── drizzle.config.ts
│   │   ├── jest.config.ts
│   │   └── tsconfig.json
│   ├── shared/              # Shared types and utilities
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── e2e/                 # End-to-end tests
│   │   ├── tests/
│   │   ├── package.json
│   │   └── playwright.config.ts
│   └── infra/               # Pulumi infrastructure
│       ├── src/
│       ├── package.json
│       ├── Pulumi.yaml
│       ├── Pulumi.dev.yaml
│       ├── Pulumi.staging.yaml
│       ├── Pulumi.prod.yaml
│       └── tsconfig.json
├── scripts/                 # Workspace automation scripts
│   ├── setup-env.js
│   ├── setup-azure-pipeline.js
│   └── deploy.js
├── docker-compose.yml       # Local PostgreSQL for development
├── package.json             # Root package.json with workspaces
├── tsconfig.base.json       # Shared TypeScript config
├── turbo.json               # Turborepo configuration
└── azure-pipelines.yml      # CI/CD configuration

Phát Triển Local Với Docker Compose

Để phát triển local, chúng tôi sử dụng Docker Compose để chạy PostgreSQL:

# docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    container_name: myapp-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-myapp}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-myapp_secret}
      POSTGRES_DB: ${POSTGRES_DB:-myapp_dev}
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-myapp} -d ${POSTGRES_DB:-myapp_dev}"]
      interval: 10s
      timeout: 5s
      retries: 5
 
volumes:
  postgres_data:

Script khởi tạo tạo cấu trúc database:

-- scripts/init-db.sql
-- This runs only on first container creation
 
-- Create extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
 
-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE myapp_dev TO myapp;

package.json gốc cấu hình npm workspaces với các script toàn diện cho mọi workflow:

{
  "name": "myapp-monorepo",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "// --- Bootstrap Scripts ---": "",
    "bootstrap": "npm ci && npm run bootstrap:env",
    "bootstrap:env": "node scripts/setup-env.js",
    "bootstrap:local": "npm run bootstrap && npm run db:start && npm run db:wait && npm run db:migrate && npm run db:seed",
    "bootstrap:azure:dev": "npm run infra:up:dev && npm run deploy:dev",
    "bootstrap:azure:staging": "npm run infra:up:staging && npm run deploy:staging",
    "bootstrap:azure:prod": "npm run infra:up:prod && npm run deploy:prod",
    "bootstrap:pipeline": "node scripts/setup-azure-pipeline.js",
 
    "// --- Development Scripts ---": "",
    "dev": "turbo run dev --parallel",
    "dev:client": "npm run dev --workspace=@myapp/client",
    "dev:server": "npm run dev --workspace=@myapp/server",
 
    "// --- Build Scripts ---": "",
    "build": "turbo run build",
    "build:client": "npm run build --workspace=@myapp/client",
    "build:server": "npm run build --workspace=@myapp/server",
    "build:shared": "npm run build --workspace=@myapp/shared",
 
    "// --- Test Scripts ---": "",
    "test": "turbo run test",
    "test:unit": "turbo run test:unit",
    "test:unit:client": "npm run test:unit --workspace=@myapp/client",
    "test:unit:server": "npm run test:unit --workspace=@myapp/server",
    "test:e2e": "npm run test:e2e --workspace=@myapp/e2e",
    "test:e2e:headed": "npm run test:e2e:headed --workspace=@myapp/e2e",
    "test:coverage": "turbo run test:coverage",
 
    "// --- Lint & Format Scripts ---": "",
    "lint": "turbo run lint",
    "lint:fix": "turbo run lint:fix",
    "format": "prettier --write \"packages/**/*.{ts,tsx,json}\"",
    "typecheck": "turbo run typecheck",
 
    "// --- Database Scripts ---": "",
    "db:start": "docker compose up -d postgres",
    "db:stop": "docker compose down",
    "db:wait": "node scripts/wait-for-db.js",
    "db:migrate": "npm run db:migrate --workspace=@myapp/server",
    "db:migrate:generate": "npm run db:migrate:generate --workspace=@myapp/server",
    "db:seed": "npm run db:seed --workspace=@myapp/server",
    "db:reset": "docker compose down -v && npm run db:start && npm run db:wait && npm run db:migrate && npm run db:seed",
    "db:studio": "npm run db:studio --workspace=@myapp/server",
 
    "// --- Infrastructure Scripts ---": "",
    "infra:login": "cd packages/infra && pulumi login",
    "infra:preview:dev": "cd packages/infra && pulumi stack select dev && pulumi preview",
    "infra:preview:staging": "cd packages/infra && pulumi stack select staging && pulumi preview",
    "infra:preview:prod": "cd packages/infra && pulumi stack select prod && pulumi preview",
    "infra:up:dev": "cd packages/infra && pulumi stack select dev && pulumi up --yes",
    "infra:up:staging": "cd packages/infra && pulumi stack select staging && pulumi up --yes",
    "infra:up:prod": "cd packages/infra && pulumi stack select prod && pulumi up",
    "infra:destroy:dev": "cd packages/infra && pulumi stack select dev && pulumi destroy",
    "infra:outputs": "cd packages/infra && pulumi stack output --json",
 
    "// --- Deploy Scripts ---": "",
    "deploy:dev": "node scripts/deploy.js dev",
    "deploy:staging": "node scripts/deploy.js staging",
    "deploy:prod": "node scripts/deploy.js prod",
 
    "// --- Utility Scripts ---": "",
    "clean": "turbo run clean && rm -rf node_modules",
    "prepare": "husky install"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "typescript": "^5.4.0",
    "prettier": "^3.2.0",
    "husky": "^9.0.0"
  }
}

Đi Sâu Vào Workspace Scripts

Hãy phân tích từng danh mục script và những gì chúng làm.

Bootstrap Scripts

Các script này thiết lập môi trường của bạn từ đầu:

# Thiết lập local đầy đủ - cài đặt deps, tạo file .env, khởi động PostgreSQL, chạy migration
npm run bootstrap:local
 
# Bootstrap Azure VM cho mỗi môi trường
npm run bootstrap:azure:dev      # Tạo hạ tầng dev + triển khai
npm run bootstrap:azure:staging  # Tạo hạ tầng staging + triển khai
npm run bootstrap:azure:prod     # Tạo hạ tầng prod + triển khai
 
# Thiết lập cấu hình Azure DevOps pipeline
npm run bootstrap:pipeline

Lệnh bootstrap:local thực hiện như sau:

  1. Cài đặt tất cả npm dependencies
  2. Tạo file .env từ ví dụ
  3. Khởi động PostgreSQL container qua Docker Compose
  4. Đợi database sẵn sàng
  5. Chạy database migration
  6. Seed database với dữ liệu ban đầu

Development Scripts

Khởi động môi trường phát triển local của bạn:

# Khởi động tất cả service (client + server) với hot reload
npm run dev
 
# Khởi động service riêng lẻ
npm run dev:client    # Chỉ frontend trên cổng 5173
npm run dev:server    # Chỉ backend trên cổng 3000

Unit Test Scripts

Chạy unit test cho frontend và backend độc lập:

# Chạy tất cả unit test
npm run test:unit
 
# Chạy frontend test (React Testing Library + Vitest)
npm run test:unit:client
 
# Chạy backend test (Jest + Supertest)
npm run test:unit:server
 
# Chạy với báo cáo coverage
npm run test:coverage

E2E Test Scripts

End-to-end test sử dụng Playwright:

# Chạy e2e test headlessly
npm run test:e2e
 
# Chạy e2e test với trình duyệt hiển thị (để debug)
npm run test:e2e:headed

Database Scripts

Quản lý PostgreSQL database local:

# Khởi động PostgreSQL container
npm run db:start
 
# Dừng PostgreSQL container
npm run db:stop
 
# Đợi database sẵn sàng (được sử dụng trong script)
npm run db:wait
 
# Chạy migration
npm run db:migrate
 
# Tạo migration mới từ thay đổi schema
npm run db:migrate:generate
 
# Seed database với dữ liệu ban đầu
npm run db:seed
 
# Reset database (phá hủy tất cả dữ liệu, tạo lại từ đầu)
npm run db:reset
 
# Mở Drizzle Studio (GUI database)
npm run db:studio

Infrastructure Scripts

Quản lý hạ tầng Azure với Pulumi:

# Đăng nhập vào Pulumi (yêu cầu một lần)
npm run infra:login
 
# Preview thay đổi trước khi áp dụng
npm run infra:preview:dev
npm run infra:preview:staging
npm run infra:preview:prod
 
# Áp dụng thay đổi hạ tầng
npm run infra:up:dev       # Tự động xác nhận cho dev
npm run infra:up:staging   # Tự động xác nhận cho staging
npm run infra:up:prod      # Yêu cầu xác nhận thủ công (an toàn)
 
# Phá bỏ hạ tầng (sử dụng cẩn thận!)
npm run infra:destroy:dev
 
# Lấy output (địa chỉ IP, tên tài nguyên, v.v.)
npm run infra:outputs

Deploy Scripts

Triển khai code ứng dụng lên Azure VM. Việc triển khai bao gồm database migration:

// scripts/deploy.js
import { execSync } from 'child_process'
 
const environment = process.argv[2]
if (!['dev', 'staging', 'prod'].includes(environment)) {
  console.error('Usage: node deploy.js <dev|staging|prod>')
  process.exit(1)
}
 
// Lấy VM IP và DB credentials từ Pulumi outputs
process.chdir('packages/infra')
execSync(`pulumi stack select ${environment}`)
const outputs = JSON.parse(execSync('pulumi stack output --json').toString())
const vmIp = outputs.vmPublicIp
process.chdir('../..')
 
console.log(`Deploying to ${environment} (${vmIp})...`)
 
// Build artifact
console.log('Building packages...')
execSync('npm run build', { stdio: 'inherit' })
 
// Copy server files (bao gồm migration)
console.log('Copying server files...')
execSync(`rsync -avz --delete packages/server/dist/ deploy@${vmIp}:/app/server/dist/`)
execSync(`rsync -avz packages/server/src/db/migrations/ deploy@${vmIp}:/app/server/migrations/`)
execSync(`rsync -avz packages/server/package.json deploy@${vmIp}:/app/server/`)
execSync(`rsync -avz packages/server/drizzle.config.ts deploy@${vmIp}:/app/server/`)
 
// Copy client files
console.log('Copying client files...')
execSync(`rsync -avz --delete packages/client/dist/ deploy@${vmIp}:/app/client/`)
 
// Copy PM2 ecosystem config
execSync(`rsync -avz ecosystem.config.js deploy@${vmIp}:/app/`)
 
// Cài đặt dependencies và chạy migration trên VM
console.log('Running migrations...')
execSync(`ssh deploy@${vmIp} "cd /app/server && npm ci --production"`, { stdio: 'inherit' })
execSync(`ssh deploy@${vmIp} "cd /app/server && npx drizzle-kit migrate"`, { stdio: 'inherit' })
 
// Khởi động lại service
console.log('Restarting services...')
execSync(`ssh deploy@${vmIp} "cd /app && pm2 restart ecosystem.config.js --update-env"`, { stdio: 'inherit' })
 
console.log(`Deployment to ${environment} complete!`)

Cấu Hình TypeScript 5

Chúng tôi tận dụng các tính năng TypeScript 5 để có trải nghiệm nhà phát triển tốt hơn. Cấu hình cơ bản:

// tsconfig.base.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "strict": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "moduleDetection": "force",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "composite": true,
    "verbatimModuleSyntax": true
  }
}

Mỗi package mở rộng cấu hình cơ bản này:

// packages/client/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "outDir": "./dist",
    "rootDir": "./src",
    "paths": {
      "@/*": ["./src/*"],
      "@shared/*": ["../shared/src/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "../shared" }]
}

CI/CD Với Azure DevOps + Pulumi

CI/CD pipeline tự động hóa testing, building và deployment với ba giai đoạn:

  1. Build & Test - Lint, typecheck, unit test và build artifact
  2. Deploy to Dev/Staging - Tự động triển khai khi push branch với E2E test
  3. Deploy to Production - Yêu cầu phê duyệt thủ công cho thay đổi hạ tầng

Mỗi môi trường có cấu hình Pulumi stack riêng với secret cho database credential:

# Thiết lập secret cho mỗi môi trường
pulumi stack select dev
pulumi config set --secret adminPassword "your-secure-password"
pulumi config set --secret dbPassword "your-db-password"

Bắt Đầu Với Template

Để sử dụng template này, chúng tôi đã tạo một lệnh CLI đơn giản:

# Clone template
npx degit our-org/typescript-fullstack-template my-new-project
 
# Điều hướng đến dự án
cd my-new-project
 
# Thiết lập local đầy đủ (cài đặt deps, khởi động PostgreSQL, chạy migration)
npm run bootstrap:local
 
# Khởi động development server
npm run dev

Hoặc, để thiết lập thủ công:

# Cài đặt dependencies
npm install
 
# Tạo file môi trường
npm run bootstrap:env
 
# Khởi động PostgreSQL với Docker Compose
npm run db:start
 
# Đợi database sẵn sàng
npm run db:wait
 
# Chạy migration
npm run db:migrate
 
# Seed với dữ liệu test (tùy chọn)
npm run db:seed
 
# Khởi động development server
npm run dev

Trải nghiệm phát triển bao gồm:

  • Hot reloading cho cả frontend và backend
  • Shared type watching - Thay đổi trong shared type phản ánh ngay lập tức
  • Concurrent development - Chạy tất cả service với một lệnh duy nhất

Lợi Ích Chúng Tôi Đã Thấy

Kể từ khi áp dụng template này, chúng tôi đã trải nghiệm:

  1. Khởi động dự án nhanh hơn - Từ nhiều ngày xuống còn vài giờ
  2. Chất lượng code nhất quán - Cùng quy tắc linting và cấu hình TypeScript trên các dự án
  3. Onboarding dễ dàng hơn - Thành viên nhóm mới hiểu cấu trúc ngay lập tức
  4. Ít lỗi runtime hơn - TypeScript phát hiện vấn đề tại thời điểm compile
  5. Triển khai đơn giản - Một mô hình pipeline cho tất cả dự án

Kết Luận

Một template được thiết kế tốt có thể cải thiện năng suất nhóm một cách đáng kể. Bằng cách sử dụng TypeScript trên toàn bộ stack—frontend, backend và infrastructure—chúng tôi đã tạo ra một trải nghiệm phát triển gắn kết giảm lỗi và tăng tốc delivery.

Các điểm rút ra chính:

  • TypeScript ở mọi nơi cung cấp tính nhất quán và type safety
  • Cấu trúc monorepo cho phép chia sẻ code và công cụ thống nhất
  • Infrastructure as code với Pulumi mang lại cho chúng ta những lợi ích tương tự trong triển khai
  • CI/CD tự động đảm bảo chất lượng và cho phép lặp lại nhanh chóng

Hãy thoải mái điều chỉnh template này theo nhu cầu nhóm của bạn. Các công cụ cụ thể có thể khác nhau, nhưng các nguyên tắc về type safety, chia sẻ code và tự động hóa sẽ phục vụ bạn tốt bất kể lựa chọn công nghệ chính xác của bạn.

Các mô hình template nào đã hoạt động tốt cho nhóm của bạn? Tôi rất muốn nghe về trải nghiệm của bạn!

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.