Giao diện
Quy ước code
Nguồn duy nhất của quy ước là CLAUDE.md ở gốc repo. Hai mục dưới đây nhúng thẳng từ file đó lúc build docs — sửa quy ước thì sửa CLAUDE.md (theo CODEOWNERS, cần core team duyệt), trang này tự cập nhật. Claude Code của dev và job AI review trên MR cũng đọc đúng file này, nên người và AI làm việc theo cùng một bộ luật.
Quy tắc kiến trúc
Vi phạm là lý do từ chối MR.
- Không import
ant-design-vuetrực tiếp ngoài@tasco/ui(layer được miễn). App/sản phẩm chỉ dùngC*, kể cả không viết thẻ<a-*>trong template (ESLint chưa bắt được). composableskhông phụ thuộcui;utilskhông phụ thuộc Nuxt.- Tối đa 2 tầng Nuxt layer; khoá chặt version
nuxt/ant-design-vue. - Màu và spacing chỉ dùng
var(--tasco-*); token mới phải thêm vào@tasco/themetrước. - Core chỉ giữ thứ thực sự dùng chung — business logic của một sản phẩm không được vào core.
- Đổi core ảnh hưởng nhiều team → mở RFC trước (
docs/core/rfc.md). - Không hardcode hay log token, secret, password.
Quy ước viết code
Chung
- TypeScript
strict+noUncheckedIndexedAccess+verbatimModuleSyntax. Cấmany(kể cả trong template),@ts-ignore,eslint-disable, enum. Chỉ dùng!/askhi đã chứng minh được là đúng. - Format: không
;, nháy đơn, thụt 2 space, trailing comma khi xuống dòng, số lớn viết10_000. Comment giải thích vì sao; API public có JSDoc. - Import:
- Dùng
import type. Thứ tự: thư viện ngoài (node:trước) →@tasco/*→ đường dẫn tương đối. - Package thư viện import tường minh từ
vue/nuxt/app/h3; layer và app dùng auto-import.
- Dùng
Đặt tên
- Kiểu tên:
- camelCase cho biến/hàm;
UPPER_SNAKEcho hằng module (regex thêm hậu tố_RE). - Type viết PascalCase, không tiền tố
I*.interfacecho object,typecho union/function. Tên dạng<Tên>Options/<Tên>Result. - Dùng union chuỗi thay enum; class chỉ dành cho Error.
- camelCase cho biến/hàm;
- Tiền tố hàm:
usecomposable ·createfactory ·definegiữ literal typeis/haskiểm tra ·tochuyển đổi ·fetchgọi HTTP ·loadnạp dữ liệumapđổi dữ liệu ngoài → nội bộ ·parse/sanitizexử lý input ·formattạo chuỗi hiển thị ·mergegộp*Oftruy xuất thuộc tính ·on*handler ·_reset*chỉ dùng cho test
- Thương hiệu:
Tascotrong tên biến/hàm;tascotrong chuỗi (tasco:<khu>:<tên>,$tasco<Tên>,--tasco-*). - File:
C<Pascal>.vue,use<Pascal>.ts; tên nhiều từ dùng kebab-case.- Plugin
NN.<tên>.ts, middleware<tên>.global.ts, route<tên>.<method>.ts. - Test và story đặt cạnh file nguồn.
- Vue: prop đặt tên
show*/*Text/default*, v-model quaupdate:<prop>, emit là động từ viết thường. CSS theo BEMc-<tên>__phần--biến-thể(trong layer:tasco-<tên>). - Dữ liệu: DTO dùng camelCase. Dữ liệu từ ngoài (snake_case của OAuth/OpenAI) giữ trong type riêng và map sang ở BFF. Env đặt dạng
NUXT_<SECTION>_<KEY>.
Lỗi & log
- Client: mọi lỗi quy về
AppErrorquatoAppError(). Mã HTTP nằm ở fieldstatus(không phảistatusCode). Phân loại bằngisAuthError/isForbidden/isValidation/isServerError/isNetworkError. - Hiển thị lỗi qua
useErrorHandler().handleError(): 401 → về trang login, 422 không bật toast, 5xx/mất mạng → notification, còn lại → message. Caller muốn tự xử lý thì gọi$tascoError(e). - Composable: bật
loading→ gánerror.value = toAppError(e)→ tắtloadingtrongfinally. Không để promise bị reject mà không có ai bắt. Hàm parse trảnull; hàm validate trả{ ok, payload | reason }. - Server:
throw createError({ statusCode, statusMessage: '<tiếng Việt>' }).- 400 input sai · 401 xác thực/IAM từ chối (
code !== 'API000') · 403 không đủ quyền · 500 thiếu cấu hình (nêu tên env) · 502 lỗi upstream. $fetchgọi ra ngoài luôn cótimeout(10s).
- 400 input sai · 401 xác thực/IAM từ chối (
- Lỗi do dùng sai API:
new Error('[@tasco/<pkg>] … <cách sửa>'). Không dùngconsole.logở runtime; server chỉ dùngconsole.warn/error, có tiền tố tên package.
API & DTO
- Route BFF trả object có key đặt tên (
{ user: AuthUser | null }). Không có dữ liệu thì trảnull, không trả 404. - Danh sách:
Paginated<T> = { items, total }; queryTableQuery { page (tính từ 1), pageSize, sortField?, sortOrder?, filters? }. - 422: field errors nằm ở gốc body
{ message, errors: { field: string[] } }. Không dùngcreateError({ data }), vì dữ liệu bị lồng vàodatavàgetFieldErrorskhông đọc được. /api/**proxy nguyên trạng. Backend lệch format thì chuyển đổi trong fetcher. Không trả envelope IAM/OIDC thẳng cho client.- DTO là
interfacetrongtypes.ts: field tuỳ chọn dùng?:, "không có" dùngT | null, input chưa tin cậy nhậnunknownrồi kiểm tra bằng type guard, forward đi đâu thì chỉ giữ các field được phép.
Patterns
- Composable-first:
use*trả object phẳng. Không dùng class service/repository hay Pinia; fetcher gọi quauseApiđóng vai "repository". - Lõi thuần (không phụ thuộc Nuxt/Vue/h3) + lớp adapter I/O mỏng; test lõi thuần trực tiếp.
- Dependency injection không cần container:
- Plugin
provide+useNuxtApp(); interface làm strategy (AuthProvider). - Truyền fetcher/callback vào (
useTable(fetcher),useErrorHandler({ onAuthError })). - Cấu hình qua
runtimeConfig/app.config.ts. Options là tham số cuối, mặc định= {}.
- Plugin
- Factory đặt tên
create*/define*; tách code client/server bằng subpath export.
Công cụ nào bắt quy ước nào
| Quy ước | Bắt tự động bằng | Chạy ở |
|---|---|---|
Không import ant-design-vue ngoài @tasco/ui | ESLint tasco/no-direct-antdv — chặn cả import type | pnpm lint |
import type cho import chỉ dùng kiểu | ESLint @typescript-eslint/consistent-type-imports | pnpm lint |
| Không để biến thừa | ESLint @typescript-eslint/no-unused-vars (cho phép tiền tố _) | pnpm lint |
strict, noUncheckedIndexedAccess, verbatimModuleSyntax | @tasco/tsconfig/base + tsc/vue-tsc | pnpm typecheck |
Mô tả cho mọi prop, event, slot của C*; mỗi component có trang docs | scripts/docs-coverage.mjs --strict | job quality |
Khoá runtimeConfig của layer có trong trang tra cứu | scripts/docs-coverage.mjs --strict | job quality |
TSDoc cho export của composables, utils, theme, ai | docs/.vitepress/gen-api.mjs --strict | pnpm build (docs) |
| Tương phản các cặp màu token | Test contrast-pairs.test.ts của @tasco/theme | pnpm test |
| Ngưỡng coverage | Vitest coverage.thresholds từng package | pnpm test |
| Không publish package chưa version | scripts/release-guard.mjs | job release |
Chưa có công cụ, người review phải để ý:
- Format (không
;, nháy đơn, trailing comma,10_000): repo chưa bật rule format trong ESLint và chưa dùng Prettier — giữ đúng như code xung quanh. - Thẻ
<a-*>viết trong template của app sản phẩm (ESLint chỉ chặnimport). - Business logic của một sản phẩm lọt vào core; tên hàm sai tiền tố (
use,create,to,is…). - Comment giải thích cái gì thay vì vì sao; comment không phải tiếng Việt.
Đổi quy ước
- Quy ước mới áp cho nhiều package hoặc đổi thói quen cả team → mở RFC trước.
- Sửa
CLAUDE.mdtrong MR; quy ước có thể kiểm tự động thì thêm luôn rule ESLint, test hoặc bước kiểm trong script. - Code hiện có chưa theo quy ước mới mà chưa sửa kịp thì ghi vào Nợ kỹ thuật.
AGENTS.md là bản rút gọn cho các AI agent khác — đổi quy ước quan trọng thì đồng bộ cả file đó.
Liên quan
- Kiến trúc nội bộ — vì sao có các ràng buộc phụ thuộc.
- Test & coverage — quy ước viết test.
- Viết tài liệu — quy ước của trang docs.