Khởi động nhanh: Tạo Nx Monorepo trong 5 phút
Mình đã mất gần 2 tuần loay hoay với một codebase gồm 3 repo riêng lẻ (API NestJS, frontend Next.js, và một package shared types) trước khi chuyển sang Nx. Sau khi migrate xong, build time giảm từ 8 phút xuống còn 90 giây nhờ cache. Đây là cách bắt đầu nhanh nhất.
Cài Nx và tạo workspace mới:
# Tạo workspace mới với preset full-stack
npx create-nx-workspace@latest my-fullstack --preset=ts
cd my-fullstack
# Thêm plugin cho NestJS và Next.js
npm install --save-dev @nx/nest @nx/next
Tạo ngay app backend và frontend:
# Tạo NestJS API
npx nx g @nx/nest:app apps/api
# Tạo Next.js frontend
npx nx g @nx/next:app apps/web
# Tạo shared library (dùng chung giữa api và web)
npx nx g @nx/js:lib libs/shared-types
Cấu trúc thư mục sau khi tạo xong:
my-fullstack/
├── apps/
│ ├── api/ # NestJS backend
│ └── web/ # Next.js frontend
├── libs/
│ └── shared-types/ # Types dùng chung
├── nx.json
├── package.json
└── tsconfig.base.json
Chạy thử ngay:
# Chạy cả hai app song song
npx nx run-many -t serve -p api web
# Build tất cả
npx nx run-many -t build --all
Hiểu cách Nx hoạt động bên trong
Project Graph — Bản đồ phụ thuộc tự động
Nx tự quét toàn bộ import statement trong workspace và vẽ dependency graph — không cần bạn khai báo gì thủ công.
# Xem dependency graph trực quan trên browser
npx nx graph
Khi apps/web import từ libs/shared-types, Nx biết ngay rằng nếu shared-types thay đổi thì web cần rebuild. Graph này là cơ sở để Nx tính affected build — phần tiết kiệm thời gian CI nhất mà mình sẽ nói tiếp.
Shared Library thực tế
Trong dự án 5 người gần đây, cái bug mình ghét nhất là: backend define User có createdAt: Date, frontend lại expect created_at: string — khác format, khác tên, chỉ phát hiện lúc runtime. Shared library xử lý triệt để chuyện này.
Tạo interface dùng chung trong libs/shared-types/src/lib/user.ts:
export interface User {
id: string;
email: string;
role: 'admin' | 'user' | 'guest';
createdAt: Date;
}
export interface ApiResponse<T> {
data: T;
message: string;
success: boolean;
}
Export từ libs/shared-types/src/index.ts:
export * from './lib/user';
export * from './lib/pagination';
export * from './lib/error-codes';
Import trong cả backend lẫn frontend — Nx tự resolve path alias:
// Trong NestJS controller
import { User, ApiResponse } from '@my-fullstack/shared-types';
// Trong Next.js component
import type { User } from '@my-fullstack/shared-types';
Path alias này được config sẵn trong tsconfig.base.json — Nx tự thêm khi generate library, không cần chỉnh tay gì.
Tối ưu Build Cache — Chỗ tiết kiệm thời gian nhiều nhất
Local Cache
Nx cache kết quả của mọi task dựa trên hash của input (source files, env vars, config). Không có gì thay đổi? Task chạy lại trong vài mili-giây thay vì vài phút.
# Lần đầu build: mất 3 phút
npx nx build api
# ✓ api:build [3m 12s]
# Lần hai (không thay đổi gì): instant
npx nx build api
# ✓ api:build [read from cache] [45ms]
Cấu hình cache trong nx.json:
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"]
}
}
}
}
Remote Cache với Nx Cloud
Local cache chỉ có tác dụng trên máy mình. Khi team có 3 người trở lên, bạn muốn developer A build xong thì developer B kéo về không cần build lại.
# Kết nối Nx Cloud (free tier đủ dùng cho team nhỏ)
npx nx connect
Sau lệnh này, Nx tự thêm config vào nx.json. Cache giờ được sync lên cloud — bao gồm cả CI. Mình bật Nx Cloud cho team và CI giảm từ 12 phút xuống 3 phút — không phải vì CI chạy nhanh hơn, mà vì phần lớn đã cache từ máy dev của anh em rồi.
Affected — Chỉ build những gì thay đổi
Mình dùng tính năng này nhiều nhất khi làm việc với PR:
# Chỉ test các project bị ảnh hưởng bởi thay đổi so với main
npx nx affected -t test --base=main --head=HEAD
# Chỉ build các project bị ảnh hưởng
npx nx affected -t build --base=main --head=HEAD
# Xem project nào sẽ bị affected
npx nx affected:graph --base=main --head=HEAD
Sửa file trong libs/shared-types? Nx tự xác định cả apps/api và apps/web đều bị affected. Chỉ sửa apps/api? apps/web không bị đụng đến — không test, không build lại.
Tích hợp CI/CD
GitHub Actions với Nx Affected
File .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Cần full history để nx affected hoạt động
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
# Kết nối Nx Cloud để dùng remote cache
- uses: nrwl/nx-set-shas@v4
- name: Lint affected
run: npx nx affected -t lint --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }}
- name: Test affected
run: npx nx affected -t test --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }}
- name: Build affected
run: npx nx affected -t build --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }}
Action nrwl/nx-set-shas tự động xác định base SHA và head SHA phù hợp — bạn không cần hardcode. Với PR, nó so sánh với base branch. Với merge vào main, nó so sánh với commit trước.
Deploy có điều kiện
Chỉ deploy service nào thực sự thay đổi:
- name: Deploy API nếu bị affected
run: |
AFFECTED=$(npx nx show projects --affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }})
if echo "$AFFECTED" | grep -q "api"; then
echo "Deploying API..."
# lệnh deploy api của bạn
fi
- name: Deploy Web nếu bị affected
run: |
AFFECTED=$(npx nx show projects --affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }})
if echo "$AFFECTED" | grep -q "web"; then
echo "Deploying Web..."
# lệnh deploy web của bạn
fi
Tips thực tế từ dự án thật
Dùng Tags để enforce architecture boundary
Nx cho phép đặt tag cho project rồi định nghĩa rule: project nào được import từ project nào. Rule này chặn developer vô tình import thẳng code database vào frontend — lỗi xuất hiện khi lint, không cần đợi code review.
// project.json của từng app/lib
{
"tags": ["scope:api", "type:app"]
}
// libs/shared-types/project.json
{
"tags": ["scope:shared", "type:lib"]
}
Cấu hình rule trong .eslintrc.json:
{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "scope:api",
"onlyDependOnLibsWithTags": ["scope:shared", "scope:api"]
},
{
"sourceTag": "scope:web",
"onlyDependOnLibsWithTags": ["scope:shared", "scope:web"]
}
]
}
]
}
}
Parallel task với giới hạn tài nguyên
# Chạy tối đa 3 task song song (tránh OOM trên máy yếu)
npx nx run-many -t build --all --parallel=3
# Chạy theo thứ tự phụ thuộc (shared-types build trước, sau đó api và web)
npx nx run-many -t build --all
Xóa cache khi cần reset
# Xóa local cache
npx nx reset
# Hoặc xóa thư mục cache thủ công
rm -rf .nx/cache
Một cạm bẫy hay gặp: bạn đổi file .env hoặc config môi trường, nhưng Nx không track file đó nên vẫn đọc từ cache cũ. Build báo success nhưng app chạy sai. Cứ nx reset rồi chạy lại — mất 2 giây gõ lệnh, không mất 20 phút debug.
Generator tự động cho code mới
Thay vì copy-paste folder, dùng generator để tạo module mới theo đúng convention của team:
# Tạo NestJS module mới
npx nx g @nx/nest:resource apps/api/src/users
# Tạo React component
npx nx g @nx/react:component Button --project=web
# Tạo library mới cho feature cụ thể
npx nx g @nx/js:lib libs/feature-auth --directory=libs/feature-auth
Ngoài hàng chục generator sẵn có, bạn còn viết được custom generator cho pattern riêng của team. Mình có một cái tạo NestJS module kèm unit test boilerplate — thành viên mới chạy một lệnh là ra đúng cấu trúc, không cần ai giải thích convention.

