Tiếng Việt

Làm chủ việc gọi API an toàn kiểu trong TypeScript để xây dựng ứng dụng web mạnh mẽ, dễ bảo trì và không lỗi. Tìm hiểu các phương pháp hay nhất và kỹ thuật nâng cao.

Gọi API Type-Safe với TypeScript: Hướng dẫn Toàn diện

Trong phát triển web hiện đại, tương tác với API là một tác vụ cơ bản. TypeScript, với hệ thống kiểu mạnh mẽ của nó, mang lại một lợi thế đáng kể trong việc đảm bảo độ tin cậy và khả năng bảo trì của các ứng dụng bằng cách cho phép gọi API an toàn kiểu (type-safe). Hướng dẫn này sẽ khám phá cách tận dụng các tính năng của TypeScript để xây dựng các tương tác API mạnh mẽ và không có lỗi, bao gồm các phương pháp hay nhất, kỹ thuật nâng cao và ví dụ thực tế.

Tại sao An toàn kiểu lại quan trọng đối với các lệnh gọi API

Khi làm việc với API, về cơ bản bạn đang xử lý dữ liệu đến từ một nguồn bên ngoài. Dữ liệu này có thể không luôn ở định dạng bạn mong đợi, dẫn đến lỗi runtime và hành vi không mong muốn. An toàn kiểu cung cấp một lớp bảo vệ quan trọng bằng cách xác minh rằng dữ liệu bạn nhận được tuân thủ một cấu trúc được xác định trước, giúp phát hiện các vấn đề tiềm ẩn ngay từ giai đoạn đầu của quá trình phát triển.

Thiết lập dự án TypeScript của bạn

Trước khi đi sâu vào các lệnh gọi API, hãy đảm bảo bạn đã thiết lập một dự án TypeScript. Nếu bạn bắt đầu từ đầu, bạn có thể khởi tạo một dự án mới bằng cách sử dụng:

npm init -y
npm install typescript --save-dev
tsc --init

Lệnh này sẽ tạo ra một tệp `tsconfig.json` với các tùy chọn trình biên dịch TypeScript mặc định. Bạn có thể tùy chỉnh các tùy chọn này để phù hợp với nhu cầu của dự án. Ví dụ, bạn có thể muốn bật chế độ nghiêm ngặt (strict mode) để kiểm tra kiểu chặt chẽ hơn:

// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Định nghĩa Type cho các phản hồi API

Bước đầu tiên để đạt được các lệnh gọi API an toàn kiểu là định nghĩa các kiểu TypeScript đại diện cho cấu trúc của dữ liệu bạn mong đợi nhận được từ API. Điều này thường được thực hiện bằng cách sử dụng khai báo `interface` hoặc `type`.

Sử dụng Interfaces

Interfaces là một cách mạnh mẽ để định nghĩa hình dạng của một đối tượng. Ví dụ, nếu bạn đang lấy danh sách người dùng từ API, bạn có thể định nghĩa một interface như sau:

interface User {
  id: number;
  name: string;
  email: string;
  address?: string; // Thuộc tính tùy chọn
  phone?: string; // Thuộc tính tùy chọn
  website?: string; // Thuộc tính tùy chọn
  company?: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

Dấu `?` sau tên thuộc tính cho biết thuộc tính đó là tùy chọn. Điều này hữu ích để xử lý các phản hồi API trong đó một số trường có thể bị thiếu.

Sử dụng Types

Types tương tự như interfaces nhưng linh hoạt hơn, bao gồm khả năng định nghĩa union types và intersection types. Bạn có thể đạt được kết quả tương tự như interface ở trên bằng cách sử dụng type:

type User = {
  id: number;
  name: string;
  email: string;
  address?: string; // Thuộc tính tùy chọn
  phone?: string; // Thuộc tính tùy chọn
  website?: string; // Thuộc tính tùy chọn
  company?: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
};

Đối với các cấu trúc đối tượng đơn giản, interfaces và types thường có thể thay thế cho nhau. Tuy nhiên, types trở nên mạnh mẽ hơn khi xử lý các tình huống phức tạp hơn.

Thực hiện các lệnh gọi API với Axios

Axios là một HTTP client phổ biến để thực hiện các yêu cầu API trong JavaScript và TypeScript. Nó cung cấp một API rõ ràng và trực quan, giúp dễ dàng xử lý các phương thức HTTP, header yêu cầu và dữ liệu phản hồi khác nhau.

Cài đặt Axios

npm install axios

Thực hiện một lệnh gọi API có kiểu

Để thực hiện một lệnh gọi API an toàn kiểu với Axios, bạn có thể sử dụng phương thức `axios.get` và chỉ định kiểu phản hồi mong đợi bằng cách sử dụng generics:

import axios from 'axios';

async function fetchUsers(): Promise {
  try {
    const response = await axios.get('https://jsonplaceholder.typicode.com/users');
    return response.data;
  } catch (error) {
    console.error('Lỗi khi lấy dữ liệu người dùng:', error);
    throw error;
  }
}

fetchUsers().then(users => {
  users.forEach(user => {
    console.log(user.name);
  });
});

Trong ví dụ này, `axios.get('...')` báo cho TypeScript biết rằng dữ liệu phản hồi dự kiến sẽ là một mảng các đối tượng `User`. Điều này cho phép TypeScript cung cấp kiểm tra kiểu và tự động hoàn thành khi làm việc với dữ liệu phản hồi.

Xử lý các phương thức HTTP khác nhau

Axios hỗ trợ các phương thức HTTP khác nhau, bao gồm `GET`, `POST`, `PUT`, `DELETE`, và `PATCH`. Bạn có thể sử dụng các phương thức tương ứng để thực hiện các loại yêu cầu API khác nhau. Ví dụ, để tạo một người dùng mới, bạn có thể sử dụng phương thức `axios.post`:

async function createUser(user: Omit): Promise {
  try {
    const response = await axios.post('https://jsonplaceholder.typicode.com/users', user);
    return response.data;
  } catch (error) {
    console.error('Lỗi khi tạo người dùng:', error);
    throw error;
  }
}

const newUser = {
  name: 'John Doe',
  email: 'john.doe@example.com',
  address: '123 Main St',
  phone: '555-1234',
  website: 'example.com',
  company: {
    name: 'Example Corp',
    catchPhrase: 'Leading the way',
    bs: 'Innovative solutions'
  }
};

createUser(newUser).then(user => {
  console.log('Người dùng đã tạo:', user);
});

Trong ví dụ này, `Omit` tạo ra một kiểu giống như `User` nhưng không có thuộc tính `id`. Điều này hữu ích vì `id` thường được máy chủ tạo ra khi tạo một người dùng mới.

Sử dụng Fetch API

Fetch API là một API tích hợp sẵn trong JavaScript để thực hiện các yêu cầu HTTP. Mặc dù nó cơ bản hơn Axios, nó cũng có thể được sử dụng với TypeScript để đạt được các lệnh gọi API an toàn kiểu. Bạn có thể ưu tiên sử dụng nó để tránh thêm một dependency nếu nó phù hợp với nhu cầu của bạn.

Thực hiện một lệnh gọi API có kiểu với Fetch

Để thực hiện một lệnh gọi API an toàn kiểu với Fetch, bạn có thể sử dụng hàm `fetch` và sau đó phân tích cú pháp phản hồi dưới dạng JSON, chỉ định kiểu phản hồi mong đợi:

async function fetchUsers(): Promise {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    if (!response.ok) {
      throw new Error(`Lỗi HTTP! trạng thái: ${response.status}`);
    }
    const data: User[] = await response.json();
    return data;
  } catch (error) {
    console.error('Lỗi khi lấy dữ liệu người dùng:', error);
    throw error;
  }
}

fetchUsers().then(users => {
  users.forEach(user => {
    console.log(user.name);
  });
});

Trong ví dụ này, `const data: User[] = await response.json();` báo cho TypeScript biết rằng dữ liệu phản hồi nên được coi là một mảng các đối tượng `User`. Điều này cho phép TypeScript thực hiện kiểm tra kiểu và tự động hoàn thành.

Xử lý các phương thức HTTP khác nhau với Fetch

Để thực hiện các loại yêu cầu API khác nhau với Fetch, bạn có thể sử dụng hàm `fetch` với các tùy chọn khác nhau, chẳng hạn như tùy chọn `method` và `body`. Ví dụ, để tạo một người dùng mới, bạn có thể sử dụng đoạn mã sau:

async function createUser(user: Omit): Promise {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(user)
    });
    if (!response.ok) {
      throw new Error(`Lỗi HTTP! trạng thái: ${response.status}`);
    }
    const data: User = await response.json();
    return data;
  } catch (error) {
    console.error('Lỗi khi tạo người dùng:', error);
    throw error;
  }
}

const newUser = {
  name: 'John Doe',
  email: 'john.doe@example.com',
  address: '123 Main St',
  phone: '555-1234',
  website: 'example.com',
  company: {
    name: 'Example Corp',
    catchPhrase: 'Leading the way',
    bs: 'Innovative solutions'
  }
};

createUser(newUser).then(user => {
  console.log('Người dùng đã tạo:', user);
});

Xử lý lỗi API

Xử lý lỗi là một khía cạnh quan trọng của các lệnh gọi API. API có thể thất bại vì nhiều lý do, bao gồm các vấn đề kết nối mạng, lỗi máy chủ và yêu cầu không hợp lệ. Điều cần thiết là phải xử lý các lỗi này một cách mượt mà để ngăn ứng dụng của bạn bị treo hoặc hiển thị hành vi không mong muốn.

Sử dụng khối Try-Catch

Cách phổ biến nhất để xử lý lỗi trong mã bất đồng bộ là sử dụng các khối try-catch. Điều này cho phép bạn bắt bất kỳ ngoại lệ nào được ném ra trong quá trình gọi API và xử lý chúng một cách thích hợp.

async function fetchUsers(): Promise {
  try {
    const response = await axios.get('https://jsonplaceholder.typicode.com/users');
    return response.data;
  } catch (error) {
    console.error('Lỗi khi lấy dữ liệu người dùng:', error);
    // Xử lý lỗi, ví dụ: hiển thị thông báo lỗi cho người dùng
    throw error; // Ném lại lỗi để cho phép mã gọi cũng có thể xử lý nó
  }
}

Xử lý các mã lỗi cụ thể

API thường trả về các mã lỗi cụ thể để chỉ ra loại lỗi đã xảy ra. Bạn có thể sử dụng các mã lỗi này để cung cấp xử lý lỗi cụ thể hơn. Ví dụ, bạn có thể muốn hiển thị một thông báo lỗi khác cho lỗi 404 Not Found so với lỗi 500 Internal Server Error.

async function fetchUser(id: number): Promise {
  try {
    const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
    return response.data;
  } catch (error: any) {
    if (error.response?.status === 404) {
      console.log(`Không tìm thấy người dùng với ID ${id}.`);
      return null; // Hoặc ném một lỗi tùy chỉnh
    } else {
      console.error('Lỗi khi lấy dữ liệu người dùng:', error);
      throw error;
    }
  }
}

fetchUser(123).then(user => {
  if (user) {
    console.log('Người dùng:', user);
  } else {
    console.log('Không tìm thấy người dùng.');
  }
});

Tạo các loại lỗi tùy chỉnh

Đối với các kịch bản xử lý lỗi phức tạp hơn, bạn có thể tạo các loại lỗi tùy chỉnh để đại diện cho các loại lỗi API khác nhau. Điều này cho phép bạn cung cấp thông tin lỗi có cấu trúc hơn và xử lý lỗi hiệu quả hơn.

class ApiError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
    this.name = 'ApiError';
  }
}

async function fetchUser(id: number): Promise {
  try {
    const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
    return response.data;
  } catch (error: any) {
    if (error.response?.status === 404) {
      throw new ApiError(404, `Không tìm thấy người dùng với ID ${id}.`);
    } else {
      console.error('Lỗi khi lấy dữ liệu người dùng:', error);
      throw new ApiError(500, 'Lỗi Máy chủ Nội bộ'); //Hoặc bất kỳ mã trạng thái phù hợp nào khác
    }
  }
}

fetchUser(123).catch(error => {
  if (error instanceof ApiError) {
    console.error(`Lỗi API: ${error.statusCode} - ${error.message}`);
  } else {
    console.error('Đã xảy ra lỗi không mong muốn:', error);
  }
});

Xác thực dữ liệu

Ngay cả với hệ thống kiểu của TypeScript, việc xác thực dữ liệu bạn nhận được từ API tại runtime là rất quan trọng. API có thể thay đổi cấu trúc phản hồi của chúng mà không báo trước, và các kiểu TypeScript của bạn có thể không luôn được đồng bộ hóa hoàn hảo với phản hồi thực tế của API.

Sử dụng Zod để xác thực Runtime

Zod là một thư viện TypeScript phổ biến để xác thực dữ liệu tại runtime. Nó cho phép bạn định nghĩa các schema mô tả cấu trúc dự kiến của dữ liệu và sau đó xác thực dữ liệu theo các schema đó tại runtime.

Cài đặt Zod

npm install zod

Xác thực phản hồi API với Zod

Để xác thực phản hồi API với Zod, bạn có thể định nghĩa một Zod schema tương ứng với kiểu TypeScript của bạn và sau đó sử dụng phương thức `parse` để xác thực dữ liệu.

import { z } from 'zod';

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  address: z.string().optional(),
  phone: z.string().optional(),
  website: z.string().optional(),
  company: z.object({
    name: z.string(),
    catchPhrase: z.string(),
    bs: z.string(),
  }).optional(),
});

type User = z.infer;

async function fetchUsers(): Promise {
  try {
    const response = await axios.get('https://jsonplaceholder.typicode.com/users');
    const data = z.array(userSchema).parse(response.data);
    return data;
  } catch (error) {
    console.error('Lỗi khi lấy dữ liệu người dùng:', error);
    throw error;
  }
}

Trong ví dụ này, `z.array(userSchema).parse(response.data)` xác thực rằng dữ liệu phản hồi là một mảng các đối tượng tuân thủ `userSchema`. Nếu dữ liệu không tuân thủ schema, Zod sẽ ném ra một lỗi, mà bạn sau đó có thể xử lý một cách thích hợp.

Các kỹ thuật nâng cao

Sử dụng Generics cho các hàm API có thể tái sử dụng

Generics cho phép bạn viết các hàm API có thể tái sử dụng có thể xử lý các loại dữ liệu khác nhau. Ví dụ, bạn có thể tạo một hàm `fetchData` generic có thể lấy dữ liệu từ bất kỳ điểm cuối API nào và trả về nó với kiểu chính xác.

async function fetchData(url: string): Promise {
  try {
    const response = await axios.get(url);
    return response.data;
  } catch (error) {
    console.error(`Lỗi khi lấy dữ liệu từ ${url}:`, error);
    throw error;
  }
}

// Cách sử dụng
fetchData('https://jsonplaceholder.typicode.com/users').then(users => {
  console.log('Người dùng:', users);
});

fetchData<{ title: string; body: string }>('https://jsonplaceholder.typicode.com/todos/1').then(todo => {
    console.log('Todo', todo)
});

Sử dụng Interceptors để xử lý lỗi toàn cục

Axios cung cấp các interceptor cho phép bạn chặn các yêu cầu và phản hồi trước khi chúng được xử lý bởi mã của bạn. Bạn có thể sử dụng interceptor để triển khai xử lý lỗi toàn cục, chẳng hạn như ghi lại lỗi hoặc hiển thị thông báo lỗi cho người dùng.

axios.interceptors.response.use(
  (response) => response,
  (error) => {
    console.error('Bộ xử lý lỗi toàn cục:', error);
    // Hiển thị thông báo lỗi cho người dùng
    return Promise.reject(error);
  }
);

Sử dụng biến môi trường cho các URL API

Để tránh việc hardcode các URL API trong mã của bạn, bạn có thể sử dụng các biến môi trường để lưu trữ các URL. Điều này giúp dễ dàng cấu hình ứng dụng của bạn cho các môi trường khác nhau, chẳng hạn như development, staging và production.

Ví dụ sử dụng tệp `.env` và gói `dotenv`.

// .env
API_URL=https://api.example.com
// Cài đặt dotenv
npm install dotenv
// Nhập và cấu hình dotenv
import * as dotenv from 'dotenv'
dotenv.config()

const apiUrl = process.env.API_URL || 'http://localhost:3000'; // cung cấp một giá trị mặc định

async function fetchData(endpoint: string): Promise {
  try {
    const response = await axios.get(`${apiUrl}/${endpoint}`);
    return response.data;
  } catch (error) {
    console.error(`Lỗi khi lấy dữ liệu từ ${apiUrl}/${endpoint}:`, error);
    throw error;
  }
}

Kết luận

Các lệnh gọi API an toàn kiểu là điều cần thiết để xây dựng các ứng dụng web mạnh mẽ, dễ bảo trì và không có lỗi. TypeScript cung cấp các tính năng mạnh mẽ cho phép bạn định nghĩa các kiểu cho phản hồi API, xác thực dữ liệu tại runtime và xử lý lỗi một cách mượt mà. Bằng cách tuân theo các phương pháp hay nhất và kỹ thuật được nêu trong hướng dẫn này, bạn có thể cải thiện đáng kể chất lượng và độ tin cậy của các tương tác API của mình.

Bằng cách sử dụng TypeScript và các thư viện như Axios và Zod, bạn có thể đảm bảo rằng các lệnh gọi API của mình an toàn kiểu, dữ liệu của bạn được xác thực và lỗi của bạn được xử lý một cách mượt mà. Điều này sẽ dẫn đến các ứng dụng mạnh mẽ và dễ bảo trì hơn.

Hãy nhớ luôn xác thực dữ liệu của bạn tại runtime, ngay cả với hệ thống kiểu của TypeScript. API có thể thay đổi, và các kiểu của bạn có thể không luôn được đồng bộ hóa hoàn hảo với phản hồi thực tế của API. Bằng cách xác thực dữ liệu tại runtime, bạn có thể phát hiện các vấn đề tiềm ẩn trước khi chúng gây ra sự cố trong ứng dụng của bạn.

Chúc bạn viết mã vui vẻ!