tao-apiservice
Quy tắc tạo API Service từ backend docs trong SASUCO. Dùng khi: viết class ApiService từ tài liệu API backend, import apiCall/buildApiUrl/buildApiUrlWithParams/ApiResponse/PagingInfo, tránh deprecated
SKILL.md.claude/skills/_archived-sasuco/tao-apiservice/SKILL.md
GitHubTạo API Service từ Backend Docs — SASUCO
⚠️ Trước Khi Code
- Đọc tài liệu backend API (trong
docs/backend/hoặc prompt) - Kiểm tra
/shared/services/api/để xác nhận functions tồn tại - Tài liệu có interface/enum nào → phải implement đủ, không bỏ sót
- Nếu phát hiện sai lệch tài liệu ↔ code thực tế → BÁO CÁO, không tự ý code
🔴 Nguyên Tắc Mapping Types 1:1 với BE DTO (BẮT BUỘC)
Tên field trong interface FE phải GIỐNG HỆT tên field trong DTO của BE. Không được đổi tên, không được viết tắt, không được sáng tạo tên mới.
✅ ĐÚNG — Copy chính xác tên từ BE docs
// BE DTO (C#): class InvoiceDocument { string invoiceNumber; DateTime issuedDate; }
interface InvoiceDocument {
invoiceNumber: string // ✅ giữ nguyên tên BE
issuedDate: string // ✅ giữ nguyên tên BE
}
❌ SAI — Đổi tên theo ý FE
interface InvoiceDocument {
number: string // ❌ BE trả về invoiceNumber, không phải number
date: string // ❌ BE trả về issuedDate, không phải date
}
🔴 Mapping Dữ Liệu Phân Trang (BẮT BUỘC)
PagingInfo<T> trong shared có cấu trúc cố định:
interface PagingInfo<T> {
items: T[] // ← tên field items
total: number // ← tổng bản ghi
page: number // ← trang hiện tại (1-based)
limit: number // ← số bản ghi mỗi trang
totalPages: number
}
Quy tắc chọn type cho response phân trang:
| BE thực tế trả về | FE dùng |
|---|---|
{ items, total, page, limit, totalPages } |
PagingInfo<T> ✅ |
{ data, totalCount, currentPage, pageSize } |
Tự tạo interface 1:1 theo BE ❌ KHÔNG dùng PagingInfo<T> |
| Bất kỳ cấu trúc khác | Tự tạo interface 1:1 theo BE |
KHÔNG được giả định BE trả về
PagingInfo<T>nếu chưa đọc docs. Nếu BE trả vềpageNumberthì interface phải cópageNumber, không phảipage.
Ví dụ BE trả cấu trúc KHÁC PagingInfo:
// BE docs: { data: T[], totalCount: number, pageNumber: number, pageSize: number }
// ❌ SAI: dùng PagingInfo<T> vì tên field không khớp
// ✅ ĐÚNG: tạo interface riêng
interface CustomerListResponse {
data: CustomerDocument[] // ← đúng tên BE
totalCount: number // ← đúng tên BE
pageNumber: number // ← đúng tên BE
pageSize: number // ← đúng tên BE
}
Import — BẮT BUỘC
import {
apiCall, // ✅ Auto: logging, headers, errors, token
buildApiUrl, // ✅ Build URL từ path
buildApiUrlWithParams, // ✅ Build URL có query params
type ApiResponse, // ✅ Response type chuẩn
type PagingInfo, // ✅ Paging type (nếu có pagination)
} from '../../../../../shared/services/api'
Import — CẤM (deprecated)
// ❌ CẤM dùng:
import { buildApiHeaders, handleApiError } from '...'
// ❌ CẤM manual try-catch — apiCall tự xử lý
// ℹ️ Không cần import ApiLogger trong static methods của ApiService
// → apiCall tự xử lý logging nội bộ
// → Nếu cần log trong hook hoặc component → dùng ApiLogger (xem skill quy-tac-code)
Template Cơ Bản
/**
* {Feature} API Service - {Portal} Portal
* @see /docs/backend/{Feature}API.md
*/
import {
apiCall, buildApiUrl, buildApiUrlWithParams,
type ApiResponse, type PagingInfo,
} from '../../../../../shared/services/api'
// ===== ENUMS & TYPES (export từ service file) =====
export enum {Feature}Status { Active = 1, Inactive = 0 }
export interface {Feature}Document { id: number; /* ... */ }
export interface Create{Feature}Request { /* ... */ }
export interface {Feature}ListParams { page?: number; pageSize?: number; search?: string }
// ===== SERVICE =====
export class {Feature}ApiService {
private static readonly BASE_PATH = '/api/{portal}/{feature}'
/** Lấy danh sách */
static async getAll(params?: {Feature}ListParams): Promise<ApiResponse<PagingInfo<{Feature}Document>>> {
const queryParams: Record<string, unknown> = {}
if (params?.search) queryParams.search = params.search
if (params?.page) queryParams.page = params.page
if (params?.pageSize) queryParams.pageSize = params.pageSize
const url = buildApiUrlWithParams(this.BASE_PATH, queryParams)
const response = await apiCall(url, { method: 'GET' })
return response.json()
}
/** Lấy chi tiết */
static async getById(id: number): Promise<ApiResponse<{Feature}Document>> {
const url = buildApiUrl(`${this.BASE_PATH}/${id}`)
const response = await apiCall(url, { method: 'GET' })
return response.json()
}
/** Tạo mới */
static async create(data: Create{Feature}Request): Promise<ApiResponse<{Feature}Document>> {
const url = buildApiUrl(this.BASE_PATH)
const response = await apiCall(url, { method: 'POST', body: JSON.stringify(data) })
return response.json()
}
/** Cập nhật */
static async update(id: number, data: Update{Feature}Request): Promise<ApiResponse<{Feature}Document>> {
const url = buildApiUrl(`${this.BASE_PATH}/${id}`)
const response = await apiCall(url, { method: 'PUT', body: JSON.stringify(data) })
return response.json()
}
/** Xóa */
static async delete(id: number): Promise<ApiResponse<void>> {
const url = buildApiUrl(`${this.BASE_PATH}/${id}`)
const response = await apiCall(url, { method: 'DELETE' })
return response.json()
}
}
Tổ Chức Types
services/
└── {Feature}ApiService[Admin].ts ← Enums + API types + Service class
types/
└── {feature}.types.ts ← Re-export API types + UI-only types
Trong types/{feature}.types.ts:
// Re-export từ service
export { {Feature}Status, type {Feature}Document } from '../services/{Feature}ApiService'
// UI-only types (KHÔNG export từ service)
export interface {Feature}FormData { /* ... */ }
export const {FEATURE}_STATUS_NAMES: Record<number, string> = { 1: 'Hoạt động', 0: 'Tạm dừng' }
Naming
| Loại | Business | Admin |
|---|---|---|
| Service class | {Feature}ApiService |
{Feature}ApiServiceAdmin |
| Interface | {Feature}Document |
— |
| Request | Create{Feature}Request, Update{Feature}Request |
— |
| Params | {Feature}ListParams |
— |
Lưu Ý Quan Trọng
- Backend docs thường viết cho cả Admin + Customer
- Chỉ tạo endpoints phù hợp với portal đang phát triển
- Admin: thường có CRUD đầy đủ
- Business/Customer: thường chỉ xem/sửa của mình
Checklist
- Import đúng:
apiCall,buildApiUrl,buildApiUrlWithParams,ApiResponse,PagingInfo - KHÔNG import:
buildApiHeaders,handleApiError,ApiLogger - Có JSDoc cho class và mỗi method
- Có
BASE_PATHconstant - Không có
try-catch, không có manual logging - Tên field trong tất cả interfaces khớp 1:1 với tên DTO của BE (không đổi tên)
- Dùng
PagingInfo<T>CHỈ KHI BE thực sự trảitems/total/page/limit/totalPages— kiểm tra docs trước - 0 TypeScript errors