Giao diện
Hợp đồng dữ liệu
Hình dạng request/response mà framework trông đợi. Backend theo được thì trang không phải viết code quy đổi; không theo được thì quy đổi ở fetcher — đừng để định dạng lạ lan vào component.
Response của một route
Trả object có khoá đặt tên, không trả giá trị trần:
ts
// Tốt
return { user } // { user: AuthUser | null }
return { clients } // { clients: AuthClientOption[] }
// Tránh
return user // thêm field sau này là breaking
return clients // mảng trần, không chỗ gắn metadataKhông có dữ liệu thì trả null trong đúng khoá đó, không trả 404. 404 dành cho đường dẫn không tồn tại, không phải cho "bản ghi trống".
Danh sách có phân trang
ts
interface Paginated<T> {
items: T[]
total: number // tổng số bản ghi khớp điều kiện, không phải số bản ghi trang này
}Tham số truy vấn mà useTable gửi cho fetcher:
ts
interface TableQuery {
page: number // tính từ 1
pageSize: number
sortField?: string // dataIndex của cột; cột lồng nối bằng '.'
sortOrder?: 'ascend' | 'descend' // không sắp xếp → cả hai field là undefined
filters?: Record<string, unknown> // filter cột gộp với bộ lọc form
}| Trường | Lưu ý |
|---|---|
page | Backend đếm từ 0 thì trừ 1 trong fetcher, đừng đổi ở component |
sortOrder | Dùng nguyên từ vựng của antdv (ascend/descend); backend dùng asc/desc thì quy đổi ở fetcher |
filters | Trùng khoá thì giá trị từ bộ lọc form thắng filter cột. Không có bộ lọc nào thì là undefined |
DTO
- Khai bằng
interfacetrong một file*.types.tscạnh composable của nghiệp vụ. - Field camelCase. Dữ liệu
snake_casetừ bên ngoài giữ trong type riêng và map sang ở fetcher (hoặc ở route Nitro nếu dự án tự viết). - Field có thể vắng mặt dùng
?:; field luôn có mặt nhưng có thể trống dùngT | null. - Dữ liệu chưa tin cậy nhận kiểu
unknownrồi kiểm tra bằng type guard, không ép kiểu bằngas. - Chuyển tiếp dữ liệu đi nơi khác thì chỉ giữ các field được phép, không đẩy nguyên object.
ts
// app/composables/orders.types.ts
export interface Order {
id: number
code: string
customer: string
amount: number
status: 'new' | 'processing' | 'done' | 'cancelled'
note?: string // có thể không có trong response
closedAt: string | null // luôn có field, chưa đóng thì null
}Dùng union chuỗi cho tập giá trị cố định, không dùng enum — quy ước chung của core.
Lỗi
Mọi lỗi HTTP về tới client đều được useApi chuyển thành AppError với mã nằm ở field status (không phải statusCode).
| Mã | Khi nào | Framework làm gì |
|---|---|---|
| 400 | Dữ liệu vào sai định dạng | Hiện message dạng toast |
| 401 | Chưa đăng nhập / phiên hết hạn | Điều hướng về /auth/login |
| 403 | Thiếu quyền | Hiện toast; route thì ném lỗi 403 |
| 422 | Dữ liệu không hợp lệ theo nghiệp vụ | Không toast — form tự hiện lỗi từng field |
| 5xx | Lỗi phía server | Hiện notification "Lỗi hệ thống" |
| không có mã | Mất mạng, timeout | Hiện notification |
Thân lỗi 422
Field errors phải nằm ở gốc body:
json
{
"message": "Dữ liệu không hợp lệ",
"errors": {
"customer": ["Khách hàng không tồn tại"],
"amount": ["Giá trị phải lớn hơn 0"]
}
}getFieldErrors(error) đọc errors (hoặc fields) ở gốc và trả Record<string, string[]> để gắn vào form. Xem Lỗi 422 và form.
Đừng dùng createError({ data }) cho 422
h3 lồng data vào trong body ({ statusCode, statusMessage, data: { errors } }), nên getFieldErrors không tìm thấy errors ở gốc. Trong route Nitro của dự án, đặt status rồi trả body thẳng:
ts
setResponseStatus(event, 422)
return { message: 'Dữ liệu không hợp lệ', errors: { amount: ['Giá trị phải lớn hơn 0'] } }Với các mã còn lại thì createError là đúng:
ts
throw createError({ statusCode: 403, statusMessage: 'Bạn không có quyền thực hiện thao tác này.' })statusMessage viết tiếng Việt, đủ nghĩa để hiện thẳng cho người dùng.
Không để lộ định dạng của hệ thống ngoài
Envelope của IAM/OIDC ({ code: 'API000', body: {...} }) là chuyện giữa BFF và nhà cung cấp. Route của dự án bóc lấy phần cần dùng rồi trả DTO sạch:
ts
// Tránh
return iamResponse // client phải biết 'API000' nghĩa là gì
// Tốt
return { user: mapIamUser(iamResponse) } // client chỉ thấy DTO của mìnhCũng vì vậy /api/** chuyển tiếp nguyên trạng: phần quy đổi là trách nhiệm của fetcher hoặc route riêng, không phải của proxy.
Danh sách rà soát khi chốt API với backend
- [ ] Danh sách trả
{ items, total };totallà tổng toàn bộ. - [ ] Trang tính từ 1, hoặc đã thống nhất quy đổi ở fetcher.
- [ ] Lỗi nghiệp vụ dùng 422 với
errorsở gốc body, thông báo tiếng Việt. - [ ] Không có dữ liệu →
null, không phải 404. - [ ] Field ngày giờ thống nhất một định dạng (ISO 8601) và một múi giờ.
- [ ] Thao tác ghi trả về bản ghi sau khi ghi, để trang không phải gọi lại.
Liên quan
- useApi và fetcher — nơi quy đổi định dạng.
- Xử lý lỗi —
AppErrorvà cách phân loại. - Trang danh sách —
PaginatedvàTableQuerydùng thực tế.