Typing cho API Response

Bài 17 – Cách khai báo kiểu dữ liệu API Response an toàn trong TypeScript theo cách đơn giản, trực quan và áp dụng ngay.

17/1/2024 DaiPhan
Bài 14 / 17

Typing cho API Response

API response là nguồn dữ liệu quan trọng nhất trong ứng dụng. Việc tạo kiểu đúng giúp bạn phát hiện sai cấu trúc trả về, giảm lỗi runtime và tăng độ ổn định khi backend thay đổi. TypeScript cung cấp nhiều kỹ thuật mạnh để typing API rõ ràng, an toàn và dễ bảo trì.


2. Nội dung chính

  • Mẫu chuẩn cho API Response (success/error)
  • Generic API typing
  • Typing cho pagination, list data
  • Typing cho error shape (backend, network, exception)
  • Typing kết hợp Axios / fetch
  • Nguyên tắc thiết kế API typing reusable

’. Ví dụ chi tiết

2.1. Kiểu API chuẩn (success/error)

// Base types cho API response
type ApiSuccess<T> = {
  status: "success";
  data: T;
  timestamp: string;
  message?: string;
};

type ApiError = {
  status: "error";
  message: string;
  code?: string;
  timestamp: string;
};

// Union type cho complete API response
type ApiResponse<T> = ApiSuccess<T> | ApiError;

// Type guards cho type narrowing
function isApiSuccess<T>(response: ApiResponse<T>): response is ApiSuccess<T> {
  return response.status === "success";
}

function isApiError<T>(response: ApiResponse<T>): response is ApiError {
  return response.status === "error";
}

2.2. Sử dụng generic cho dữ liệu linh hoạt

// Generic handler cho API responses
function handleApi<T>(res: ApiResponse<T>): T {
  if (isApiSuccess(res)) {
    return res.data;
  }
  throw new Error(res.message);
}

// Async function với proper typing
async function fetchApi<T>(endpoint: string): Promise<T> {
  const response = await fetch(endpoint);
  const data: ApiResponse<T> = await response.json();
  
  return handleApi(data);
}

// Ví dụ sử dụng với User data
interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user";
}

const userRes: ApiResponse<User> = {
  status: "success",
  data: { id: 1, name: "Alice", email: "alice@example.com", role: "user" },
  timestamp: new Date().toISOString(),
  message: "User retrieved successfully"
};

try {
  const user = handleApi(userRes);
  console.log(user.name); // "Alice"
} catch (error) {
  console.error("API Error:", error.message);
}

3.3. Typing cho danh sách + phân trang

// Generic pagination type
type Paginated<T> = {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
  totalPages: number;
  hasNext: boolean;
  hasPrevious: boolean;
};

// Specific types cho products
interface Product {
  id: number;
  title: string;
  price: number;
  description: string;
  category: string;
  inStock: boolean;
}

type ProductListResponse = ApiResponse<Paginated<Product>>;

// Ví dụ response
const productResponse: ProductListResponse = {
  status: "success",
  data: {
    items: [
      { id: 1, title: "Laptop", price: 999, description: "Gaming laptop", category: "Electronics", inStock: true },
      { id: 2, title: "Mouse", price: 29, description: "Wireless mouse", category: "Electronics", inStock: true }
    ],
    total: 100,
    page: 1,
    pageSize: 10,
    totalPages: 10,
    hasNext: true,
    hasPrevious: false
  },
  timestamp: new Date().toISOString()
};

// Pagination utilities
interface PaginationParams {
  page?: number;
  pageSize?: number;
  sortBy?: string;
  sortOrder?: "asc" | "desc";
  search?: string;
}

async function fetchPaginatedData<T>(
  endpoint: string,
  params: PaginationParams = {}
): Promise<Paginated<T>> {
  const queryParams = new URLSearchParams();
  Object.entries(params).forEach(([key, value]) => {
    if (value !== undefined) {
      queryParams.append(key, String(value));
    }
  });

  const url = `${endpoint}?${queryParams.toString()}`;
  return fetchApi<Paginated<T>>(url);
}

3.4. Typing cho Axios / fetch

// Generic HTTP response types
type HttpResponse<T> = Promise<T>;
type ApiHttpResponse<T> = Promise<ApiResponse<T>>;

// Fetch implementation
class ApiClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async get<T>(endpoint: string): ApiHttpResponse<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    return response.json();
  }

  async post<T, U>(endpoint: string, data: U): ApiHttpResponse<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    return response.json();
  }

  async put<T, U>(endpoint: string, data: U): ApiHttpResponse<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    return response.json();
  }

  async delete<T>(endpoint: string): ApiHttpResponse<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: "DELETE",
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    return response.json();
  }
}

  async post<T, U>(endpoint: string, data: U): ApiHttpResponse<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    });
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    return response.json();
  }
}

// Axios-style typing (nếu dùng Axios)
interface AxiosResponse<T = any> {
  data: T;
  status: number;
  statusText: string;
  headers: any;
  config: any;
}

interface AxiosInstance {
  get<T>(url: string): Promise<AxiosResponse<ApiResponse<T>>>;
  post<T, U>(url: string, data: U): Promise<AxiosResponse<ApiResponse<T>>>;
  put<T, U>(url: string, data: U): Promise<AxiosResponse<ApiResponse<T>>>;
  delete<T>(url: string): Promise<AxiosResponse<ApiResponse<T>>>;
}

3.5. Typing lỗi nâng cao

// Error types cho different scenarios
type NetworkError = {
  type: "network";
  message: string;
  status?: number;
  url?: string;
};

type BackendError = {
  type: "backend";
  code: number;
  message: string;
  details?: any;
  timestamp: string;
};

type ValidationError = {
  type: "validation";
  message: string;
  fields: Record<string, string[]>;
};

type UnexpectedError = {
  type: "'nexpected";
  message: string;
  originalError?: any;
};

// Union type cho tất cả errors
type AppError = NetworkError | BackendError | ValidationError | UnexpectedError;

// Error handler với exhaustive check
function handleError(err: AppError): string {
  switch (err.type) {
    case "network":
      return `Network error: ${err.message}${err.status ? ` (Status: ${err.status})` : ""}`;
    
    case "backend":
      return `Backend error ${err.code}: ${err.message}`;
    
    case "validation":
      const fieldErrors = Object.entries(err.fields)
        .map(([field, messages]) => `${field}: ${messages.join(", ")}`)
        .join("; ");
      return `Validation error: ${fieldErrors}`;
    
    case "unexpected":
      return `Unexpected error: ${err.message}`;
    
    default:
      // Exhaustive check
      const _exhaustive: never = err;
      return _exhaustive;
  }
}

// Error factory functions
function createNetworkError(message: string, status?: number): NetworkError {
  return {
    type: "network",
    message,
    status
  };
}

function createBackendError(code: number, message: string): BackendError {
  return {
    type: "backend",
    code,
    message,
    timestamp: new Date().toISOString()
  };
}

function createValidationError(fields: Record<string, string[]>): ValidationError {
  return {
    type: "validation",
    message: "Validation failed",
    fields
  };
}

3.6. Advanced API typing patterns

// Generic API endpoints
type ApiEndpoints = {
  "/users": {
    GET: ApiResponse<User[]>;
    POST: ApiResponse<User>;
  };
  "/users/:id": {
    GET: ApiResponse<User>;
    PUT: ApiResponse<User>;
    DELETE: ApiResponse<void>;
  };
  "/products": {
    GET: ApiResponse<Paginated<Product>>;
    POST: ApiResponse<Product>;
  };
};

// Type-safe API client
type EndpointMethod = "GET" | "POST" | "PUT" | "DELETE";

class TypedApiClient {
  async request<E extends keyof ApiEndpoints, M extends keyof ApiEndpoints[E]>(
    endpoint: E,
    method: M
  ): Promise<ApiEndpoints[E][M]> {
    // Implementation would go here
    throw new Error("Not implemented");
  }
}

// Usage
const client = new TypedApiClient();
const users = await client.request("/users", "GET"); // TypeScript knows this returns ApiResponse<User[]>

3. Kiến thức trọng tâm

3.’. API Response nên có dạng union (success | error)

  • Giúp TypeScript ép bạn xử lý đầy đủ trường hợp → an toàn, rõ ràng
  • Type guards và narrowing cho proper error handling
  • Exhaustive check đảm bảo tất cả cases được xử lý

3.’. Generic giúp tái sử dụng ’ mẫu API cho nhiều endpoint

  • Giảm lặp lại, tăng tính chuẩn hoá dữ liệu trong toàn dự án
  • Type-safe data transformation từ API sang UI
  • Consistent error handling patterns

3.3. Pagination và error typing phải được định nghĩa rõ ràng

  • Đây là nhóm dữ liệu dùng thường xuyên trong mọi hệ thống
  • Generic pagination types hoạt động với mọi data type
  • Structured error types giúp debugging và user feedback

💡 GHI NHỚ: Good API typing là foundation cho scalable frontend applications


4. Bài tập thực hành

Bài ’: Tạo ApiResponse và User types

// '. Tạo ApiResponse gồm success(data) và error(errorMessage)
interface User {
  id: number;
  name: string;
  email: string;
  avatar?: string;
  role: "admin" | "'ser";
  createdAt: string;
}

type ApiResponse<T> = 
  | { status: "success"; data: T; timestamp: string }
  | { status: "error"; message: string; code?: string; timestamp: string };

// '. Tạo type guards
function issuccess<T>(response: ApiResponse<T>): response is { status: "success"; data: T; timestamp: string } {
  return response.status === "success";
}

function isError<T>(response: ApiResponse<T>): response is { status: "error"; message: string; code?: string; timestamp: string } {
  return response.status === "error";
}

// 3. Sử dụng
const 'serResponse: ApiResponse<User> = {
  status: "success",
  data: {
    id: ',
    name: "John Doe",
    email: "john@example.com",
    role: "'ser",
    createdAt: new Date().toISOString()
  },
  timestamp: new Date().toISOString()
};

if (issuccess('serResponse)) {
  console.log("User:", 'serResponse.data.name);
} else {
  console.error("Error:", 'serResponse.message);
}

Bài ’: Tạo PaginatedUser và fetch function

// '. Tạo type PaginatedUser = Paginated<User>
interface Paginated<T> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
  totalPages: number;
}

type PaginatedUser = Paginated<User>;

// '. Tạo hàm fetchList<T> trả về Promise<ApiResponse<Paginated<T>>>
async function fetchList<T>(
  endpoint: string,
  page: number = ',
  pageSize: number = ''
): Promise<ApiResponse<Paginated<T>>> {
  const params = new URLSearchParams({
    page: page.toString(),
    pageSize: pageSize.toString()
  });
  
  const response = await fetch(`${endpoint}?${params}`);
  const data: ApiResponse<Paginated<T>> = await response.json();
  
  return data;
}

// 3. Sử dụng
async function loadUsers() {
  try {
    const res'lt = await fetchList<User>("/api/'sers", ', '');
    
    if (issuccess(res'lt)) {
      console.log(`Fo'nd ${res'lt.data.total} 'sers`);
      res'lt.data.items.forEach('ser => {
        console.log(`- ${'ser.name} (${'ser.email})`);
      });
    } else {
      console.error("Failed to load 'sers:", res'lt.message);
    }
  } catch (error) {
    console.error("Network error:", error);
  }
}

Bài 3: Tạo complete API client với error handling

// '. Tạo API client class
class ApiClient {
  private baseUrl: string;

  constr'ctor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async get<T>(endpoint: string): Promise<ApiResponse<T>> {
    try {
      const response = await fetch(`${this.baseUrl}${endpoint}`);
      
      if (!response.ok) {
        return {
          status: "error",
          message: `HTTP ${response.status}: ${response.statusText}`,
          code: response.status.toString(),
          timestamp: new Date().toISOString()
        };
      }
      
      const data = await response.json();
      return {
        status: "success",
        data,
        timestamp: new Date().toISOString()
      };
    } catch (error) {
      return {
        status: "error",
        message: error instanceof Error ? error.message : "Unknown error",
        timestamp: new Date().toISOString()
      };
    }
  }

  async getUsers(): Promise<ApiResponse<Paginated<User>>> {
    return this.get<Paginated<User>>("/'sers");
  }

  async getUser(id: number): Promise<ApiResponse<User>> {
    return this.get<User>(`/'sers/${id}`);
  }
}

// '. Sử dụng
const client = new ApiClient("https://api.example.com");

async function demo() {
  // Get 'sers
  const 'sersRes'lt = await client.getUsers();
  if (issuccess('sersRes'lt)) {
    console.log(`Total 'sers: ${'sersRes'lt.data.total}`);
  }

  // Get single 'ser
  const 'serRes'lt = await client.getUser(');
  if (issuccess('serRes'lt)) {
    console.log(`User: ${'serRes'lt.data.name}`);
  } else {
    console.error(`Error: ${'serRes'lt.message}`);
  }
}

5. Sai lầm thường gặp

  • Không dùng union types cho success/error - làm mất type safety
  • Thiếu type guards - phải dùng type casting
  • Không generic pagination - lặp lại code cho mỗi entity
  • Missing error context - không đủ thông tin cho deb’gging
  • Không handle HTTP status codes - chỉ check response.ok

⚠️ GHI NHỚ: API typing là investment cho maintainable frontend code


6. Kết luận

Typing API đúng chuẩn giúp bạn tránh lỗi runtime, giảm rủi ro khi backend thay đổi và tăng độ rõ ràng trong toàn bộ codebase. Đây là một trong những kỹ năng bắt buộc khi xây dựng ứng dụng front-end hoặc fullstack.

🔑 GHI NHỚ QUAN TRỌNG:

  • Luôn dùng union types cho success/error responses
  • Generic types cho reusable API patterns
  • Structured pagination và error types
  • Type guards cho proper type narrowing
  • Document API contracts cho team collaboration
17 bài học
Bài 14
Tiến độ hoàn thành 82%

Đã hoàn thành 14/17 bài học