camelCase vs snake_case trong NestJS và Prisma | Đổi tên hàng loạt an toàn
Xung đột camelCase / snake_case trong dự án Nest + Prisma gần như chắc chắn xuất hiện khi outsource FE/BE hoặc inherit DB cũ. Giữ DB = snake_case, TypeScript + JSON API = camelCase, nối bằng @map — đừng “đổi hết cho đồng”. Đổi tên hàng loạt chỉ trên danh sách identifier đã duyệt, không find-replace mù cả repo.
Ở Việt Nam, nhiều team product nhỏ thuê freelancer FE React và BE Nest riêng, hoặc nhận codebase từ client Nhật/Hàn với PostgreSQL đặt cột created_at, user_id. Một sprint sau, ticket bắt đầu xuất hiện: FE đọc userName nhưng API trả user_name, hoặc Prisma model viết user_name khiến ESLint/React nhìn “lạ”.
Bài này đi thẳng vào xung đột convention khi outsource, cách map Nest/Prisma đúng tầng, và quy trình bulk rename an toàn — kèm Case Converter khi cần sinh hàng loạt tên field.
Bốn case hay gặp (nhớ để đọc code người khác)
| Kiểu | Ví dụ | Hay dùng ở đâu |
|---|---|---|
| camelCase | userName, createdAt | Biến/hàm JS·TS, JSON API Nest, field Prisma trong TS |
| snake_case | user_name, created_at | Cột SQL, file Python, một số API đối tác, env key |
| PascalCase | UserProfile, CreateUserDto | Class, React component, enum type |
| kebab-case | user-profile, created-at | URL path, CSS class, slug |
Không có kiểu nào “sai tuyệt đối” — sai là trộn không có lớp chuyển đổi.
Góc NestJS + Prisma: conflict điển hình
Triệu chứng
// FE expect
user.userName
// Response thật
{ "user_name": "An", "created_at": "2026-07-01T00:00:00.000Z" }
Hoặc ngược lại: DB migration cũ là snake, junior viết Prisma:
model User {
userName String // tạo cột "userName" — lệch chuẩn SQL team
}
Cách tách tầng (khuyến nghị)
model User {
id String @id @default(cuid())
userName String @map("user_name")
createdAt DateTime @default(now()) @map("created_at")
@@map("users")
}
- Trong code TS:
user.userName,user.createdAt - Trong DB:
user_name,created_at - JSON API: serialize camelCase (mặc định class-transformer / Nest thường giữ tên property TS)
Client mobile hoặc đối tác outsource chỉ cần đọc OpenAPI — một convention cho JSON.
Khi API phải trả snake_case
Một số client doanh nghiệp (ERP, đối tác ngân hàng) bắt body snake. Khi đó:
- Giữ Prisma/TS camel +
@map - Thêm interceptor/DTO riêng cho outbound snake
- Không đổi schema DB chỉ vì một consumer
Document rõ: internal = camel, partner X = snake.
Outsourcing: checklist tránh “đặt tên lệch sprint”
- Spec trước code: sheet hoặc OpenAPI liệt kê field JSON (camel) và cột DB (snake) cạnh nhau
- Một người duyệt acronym:
userIdvsuserID,httpStatusvsHTTPStatus— chọn một, khóa trong lint nếu được - Cấm FE tự đổi key: nếu BE sai, sửa BE hoặc BFF, không
data.user_name || data.userNamelan mọi nơi - PR template: “Field mới đã có
@mapchưa?” - Smoke test: một file
fixtures/user.jsonkhớp contract
Freelancer mới join project VN thường copy response Postman vào type bằng tay → dễ gõ created_At. Dùng converter để sinh từ danh sách cột DB sẽ ít lỗi hơn.
Bulk rename an toàn
Khi nào nên bulk convert
- Migrate danh sách 30+ cột từ Excel spec → Prisma fields
- Đổi convention JSON public chưa ra production
- Sinh song song
snakecho SQL vàcamelcho DTO
Khi nào không
- Repo đã có production API + mobile cũ
- Key đến từ hệ thống ngoài (
access_token,error_codecố định) - Tên file, biến môi trường, message queue topic
Quy trình 6 bước
- Export danh sách identifier (một cột, mỗi dòng một tên)
- Dán vào Case Converter → chọn đích (camel / snake / Pascal / kebab)
- Review tay 5–10 tên đặc biệt (ID, URL, HTTP, Việt Nam không dấu trong identifier)
- Apply bằng rename symbol của IDE / codemod — không Notepad replace toàn repo
- Chạy test + so JSON fixture
- Commit tách:
chore: align user DTO naming— dễ revert
Ví dụ chuyển nhanh:
user_name → userName
created_at → createdAt
is_active → isActive
oauth_client → oauthClient (review: giữ OAuth?)
Tool hỗ trợ bốn dạng chính; xử lý trên trình duyệt — phù hợp khi không muốn dán schema nội bộ lên dịch vụ lạ.
Case study: team 2 BE + 1 FE freelance
Bối cảnh: Startup fintech nhỏ tại HCM, BE Nest + Prisma, FE Next. DB kế thừa từ prototype Python (toàn snake). FE viết type tay theo Postman lúc BE còn trả snake. Sau khi BE bật camel serializer, form “Cập nhật hồ sơ” lưu undefined vì đọc sai key.
Cách gỡ trong 1 ngày:
- Freeze contract: mọi endpoint v1 trả camel
- Sinh lại types từ OpenAPI (hoặc bảng map từ case converter)
- Thêm test e2e assert
createdAttồn tại,created_atkhông - Partner báo cáo (vẫn cần snake) đi qua BFF
/partner/v1riêng
Không cần rename cột DB — chỉ sửa lớp API và FE.
Lỗi hay gặp khi convert
| Lỗi | Ví dụ | Cách tránh |
|---|---|---|
| Đôi underscore | user__name → userName lệch kỳ vọng | Normalize input trước |
| Chữ số | iso8601_date | Review thủ công |
| Acronym | user_id → userId (thường đúng) vs html_url → htmlUrl | Style guide |
| Đổi cả string nội dung | Replace user_name trong câu log tiếng Anh | Chỉ rename symbol |
| Trộn kebab trong JSON | "user-name": | JSON key nên camel hoặc snake, tránh kebab |
Gợi ý convention một trang cho README
## Naming
- PostgreSQL columns: snake_case
- Prisma fields & Nest DTO: camelCase + @map
- REST JSON (public): camelCase
- URL path: kebab-case
- React components: PascalCase
Dán vào onboarding freelancer — giảm 50% comment “đổi tên giúp em” trên PR.
Liên kết hữu ích
- Chuyển đổi Case (camel / snake / Pascal / kebab)
- JSON Formatter — kiểm tra key response trước khi viết type
- Danh sách công cụ
Tóm lại: Nest/Prisma không buộc bạn chọn một case cho mọi tầng. Map rõ DB ↔ TS ↔ JSON, bulk rename có danh sách và review acronym, và dùng converter khi sinh hàng loạt — đó là cách sống sót khi outsource mà không phá production.