TypeScript مع واجهات برمجة التطبيقات REST
TypeScript مع واجهات برمجة التطبيقات REST
توفر TypeScript دعمًا ممتازًا للعمل مع واجهات برمجة التطبيقات REST من خلال تمكين طلبات HTTP آمنة من حيث النوع، واستجابات مكتوبة، ومعالجة منظمة للأخطاء. في هذا الدرس، سنستكشف كيفية كتابة استجابات API، استخدام Axios و Fetch مع TypeScript، وبناء أنماط عميل API قوية تكتشف الأخطاء في وقت الترجمة بدلاً من وقت التشغيل.
لماذا نكتب طبقة API؟
كتابة تفاعلات API الخاصة بك توفر عدة فوائد حاسمة:
- الأمان من حيث النوع: اكتشاف عدم تطابق بنية استجابة API قبل النشر
- الإكمال التلقائي: احصل على IntelliSense لجميع خصائص استجابة API
- إعادة الهيكلة: أعد تسمية أو إعادة هيكلة نماذج API بثقة في جميع أنحاء قاعدة الكود الخاصة بك
- التوثيق: الأنواع تعمل كتوثيق حي لعقود API الخاصة بك
- منع الأخطاء: القضاء على أخطاء الوصول إلى خصائص غير محددة
كتابة نماذج استجابة API
ابدأ بتعريف واجهات TypeScript أو أنواع لبنيات استجابة API الخاصة بك:
// نوع مورد المستخدم
interface User {
id: number;
email: string;
username: string;
firstName: string;
lastName: string;
avatar?: string;
createdAt: string;
updatedAt: string;
}
// غلاف استجابة مقسمة للصفحات
interface PaginatedResponse<T> {
data: T[];
meta: {
total: number;
page: number;
perPage: number;
totalPages: number;
};
links: {
first: string;
last: string;
prev: string | null;
next: string | null;
};
}
// بنية خطأ API
interface ApiError {
message: string;
errors?: Record<string, string[]>;
statusCode: number;
}
// استجابة مورد واحد
interface SingleResponse<T> {
data: T;
message?: string;
}
// استجابة نجاح بدون بيانات
interface SuccessResponse {
message: string;
success: boolean;
}
types/api.ts حتى يمكن استيرادها في جميع أنحاء التطبيق الخاص بك. حافظ على هذه الأنواع متزامنة مع وثائق API الخلفية.
كتابة طلبات Fetch API
يمكن كتابة Fetch API الأصلية لاستدعاءات API آمنة ويمكن التنبؤ بها:
// غلاف fetch عام مع أمان النوع
async function fetchApi<T>(
url: string,
options?: RequestInit
): Promise<T> {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
...options,
});
if (!response.ok) {
const error: ApiError = await response.json();
throw new Error(error.message || 'فشل طلب API');
}
return response.json() as Promise<T>;
}
// أمثلة الاستخدام
async function getUser(id: number): Promise<User> {
const response = await fetchApi<SingleResponse<User>>(
`https://api.example.com/users/${id}`
);
return response.data;
}
async function getUsers(
page: number = 1
): Promise<PaginatedResponse<User>> {
return fetchApi<PaginatedResponse<User>>(
`https://api.example.com/users?page=${page}`
);
}
async function createUser(
userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>
): Promise<User> {
const response = await fetchApi<SingleResponse<User>>(
'https://api.example.com/users',
{
method: 'POST',
body: JSON.stringify(userData),
}
);
return response.data;
}
Omit و Pick لإنشاء أنواع إدخال من أنواع الاستجابة الخاصة بك. هذا يضمن الاتساق بين ما ترسله وما تستقبله من API.
كتابة طلبات Axios
يوفر Axios دعم TypeScript ممتاز جاهز للاستخدام. إليك كيفية الاستفادة منه بفعالية:
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
// إنشاء نسخة Axios مكتوبة
class ApiClient {
private client: AxiosInstance;
constructor(baseURL: string) {
this.client = axios.create({
baseURL,
headers: {
'Content-Type': 'application/json',
},
timeout: 10000,
});
// إضافة مقاطع طلب لرموز المصادقة
this.client.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// إضافة مقاطع استجابة لمعالجة الأخطاء
this.client.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// معالجة الوصول غير المصرح به
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
}
async get<T>(
url: string,
config?: AxiosRequestConfig
): Promise<T> {
const response: AxiosResponse<T> = await this.client.get(url, config);
return response.data;
}
async post<T, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig
): Promise<T> {
const response: AxiosResponse<T> = await this.client.post(
url,
data,
config
);
return response.data;
}
async put<T, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig
): Promise<T> {
const response: AxiosResponse<T> = await this.client.put(
url,
data,
config
);
return response.data;
}
async delete<T>(
url: string,
config?: AxiosRequestConfig
): Promise<T> {
const response: AxiosResponse<T> = await this.client.delete(url, config);
return response.data;
}
}
// تصدير نسخة singleton
export const apiClient = new ApiClient('https://api.example.com');
بناء طبقات خدمة API
نظم استدعاءات API الخاصة بك في فئات خدمة للحصول على بنية أفضل وقابلية إعادة الاستخدام:
class UserService {
private basePath = '/users';
async getAll(params?: {
page?: number;
perPage?: number;
search?: string;
}): Promise<PaginatedResponse<User>> {
return apiClient.get<PaginatedResponse<User>>(this.basePath, {
params,
});
}
async getById(id: number): Promise<User> {
const response = await apiClient.get<SingleResponse<User>>(
`${this.basePath}/${id}`
);
return response.data;
}
async create(
userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>
): Promise<User> {
const response = await apiClient.post<SingleResponse<User>>(
this.basePath,
userData
);
return response.data;
}
async update(
id: number,
userData: Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>
): Promise<User> {
const response = await apiClient.put<SingleResponse<User>>(
`${this.basePath}/${id}`,
userData
);
return response.data;
}
async delete(id: number): Promise<SuccessResponse> {
return apiClient.delete<SuccessResponse>(`${this.basePath}/${id}`);
}
async uploadAvatar(id: number, file: File): Promise<User> {
const formData = new FormData();
formData.append('avatar', file);
const response = await apiClient.post<SingleResponse<User>>(
`${this.basePath}/${id}/avatar`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
return response.data;
}
}
export const userService = new UserService();
أنماط عميل API المتقدمة
نفذ أنماط متطورة لعملاء API جاهزين للإنتاج:
interface RetryConfig {
maxRetries: number;
retryDelay: number;
retryOn: number[];
}
async function fetchWithRetry<T>(
url: string,
options?: RequestInit,
retryConfig: RetryConfig = {
maxRetries: 3,
retryDelay: 1000,
retryOn: [408, 429, 500, 502, 503, 504],
}
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (
!response.ok &&
retryConfig.retryOn.includes(response.status) &&
attempt < retryConfig.maxRetries
) {
await new Promise((resolve) =>
setTimeout(resolve, retryConfig.retryDelay * (attempt + 1))
);
continue;
}
if (!response.ok) {
const error: ApiError = await response.json();
throw new Error(error.message);
}
return response.json() as Promise<T>;
} catch (error) {
lastError = error as Error;
if (attempt < retryConfig.maxRetries) {
await new Promise((resolve) =>
setTimeout(resolve, retryConfig.retryDelay * (attempt + 1))
);
}
}
}
throw lastError!;
}
class CancellableRequest<T> {
private controller: AbortController;
private promise: Promise<T>;
constructor(url: string, options?: RequestInit) {
this.controller = new AbortController();
this.promise = fetch(url, {
...options,
signal: this.controller.signal,
}).then((response) => {
if (!response.ok) {
throw new Error('فشل الطلب');
}
return response.json() as Promise<T>;
});
}
cancel(): void {
this.controller.abort();
}
getPromise(): Promise<T> {
return this.promise;
}
}
// الاستخدام
const request = new CancellableRequest<PaginatedResponse<User>>(
'https://api.example.com/users'
);
// الإلغاء إذا لزم الأمر
setTimeout(() => request.cancel(), 5000);
try {
const data = await request.getPromise();
console.log(data);
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
console.log('تم إلغاء الطلب');
}
}
AbortError التي يجب التقاطها ومعالجتها بشكل مناسب بدلاً من معاملتها كفشل فعلي في API.
حراس النوع لاستجابات API
استخدم حراس النوع للتحقق من صحة استجابات API في وقت التشغيل:
function isUser(obj: any): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
typeof obj.id === 'number' &&
typeof obj.email === 'string' &&
typeof obj.username === 'string' &&
typeof obj.firstName === 'string' &&
typeof obj.lastName === 'string' &&
typeof obj.createdAt === 'string' &&
typeof obj.updatedAt === 'string'
);
}
function isPaginatedResponse<T>(
obj: any,
itemGuard: (item: any) => item is T
): obj is PaginatedResponse<T> {
return (
typeof obj === 'object' &&
obj !== null &&
Array.isArray(obj.data) &&
obj.data.every(itemGuard) &&
typeof obj.meta === 'object' &&
typeof obj.meta.total === 'number'
);
}
// الاستخدام مع التحقق من الصحة
async function getSafeUsers(): Promise<PaginatedResponse<User>> {
const data = await fetchApi<any>('https://api.example.com/users');
if (!isPaginatedResponse(data, isUser)) {
throw new Error('بنية استجابة API غير صالحة');
}
return data;
}
- أنشئ عميل API مكتوب لـ posts API مع نقاط النهاية: GET /posts، GET /posts/:id، POST /posts، PUT /posts/:id، DELETE /posts/:id
- عرّف واجهات TypeScript لـ Post (مع id، title، content، authorId، createdAt) واستجابات PaginatedPosts
- نفذ فئة PostService مع طرق آمنة من حيث النوع لجميع عمليات CRUD
- أضف معالجة الأخطاء مع أنواع ApiError مخصصة ومنطق إعادة المحاولة للطلبات الفاشلة
- أنشئ حراس نوع للتحقق من صحة استجابات API في وقت التشغيل
- أضف دعم إلغاء الطلب باستخدام AbortController
ملخص
في هذا الدرس، تعلمت كيفية العمل مع واجهات برمجة التطبيقات REST في TypeScript من خلال كتابة استجابات API، واستخدام Axios و Fetch مع أمان نوع كامل، وتنفيذ أنماط عميل API قوية. استكشفت بنية طبقة الخدمة، وآليات إعادة محاولة الطلب، وأنماط الإلغاء، والتحقق من الصحة في وقت التشغيل مع حراس النوع. تمكّنك هذه الأنماط من بناء تكاملات API موثوقة وقابلة للصيانة تكتشف الأخطاء في وقت الترجمة وتوفر تجربة مطور ممتازة من خلال الإكمال التلقائي والتحقق من النوع.