بناء أدوات المطورين

بناء مكتبات عملاء API

5 دقيقة للقراءة

Project Goal: Create a professional API client library that other developers will love using.

Why Build API Client Libraries?

API clients are valuable because:

  • Solve real problems - Make API integration effortless
  • High reuse - Used across many projects
  • TypeScript-friendly - Types improve DX significantly
  • npm distribution - Easy to share and version

API Client Project Prompt

Create a TypeScript API client library for [API Name]:

## Features
- Type-safe request/response handling
- Automatic retry with exponential backoff
- Rate limiting support
- Request/response interceptors
- Error classification
- Authentication handling (API key, OAuth, etc.)
- Pagination helpers
- Comprehensive logging options

## Tech Stack
- TypeScript with strict mode
- fetch or axios for HTTP
- Zod for runtime validation
- tsup for building
- Vitest for testing

## Project Structure

/src /client # Main client class /resources # API resource classes /types # TypeScript types /utils # Utilities (retry, rate-limit) /errors # Custom error classes index.ts # Public exports /examples # Usage examples


## API Endpoints to Support
[List the main API endpoints]

## Authentication Method
[Describe auth: API key, OAuth, Bearer token, etc.]

Project Setup

// package.json
{
  "name": "@yourorg/api-client",
  "version": "1.0.0",
  "description": "Type-safe API client",
  "type": "module",
  "main": "./dist/index.js",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsup src/index.ts --format esm --dts --clean",
    "dev": "tsup src/index.ts --format esm --dts --watch",
    "test": "vitest",
    "test:coverage": "vitest run --coverage",
    "lint": "eslint src --ext .ts",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^22.9.0",
    "tsup": "^8.3.5",
    "typescript": "^5.6.3",
    "vitest": "^2.1.5"
  },
  "peerDependencies": {
    "typescript": ">=5.0.0"
  }
}

Type Definitions

// src/types/index.ts
import { z } from 'zod';

// API Response schemas
export const userSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  name: z.string(),
  avatar: z.string().url().optional(),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
});

export const paginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
  z.object({
    data: z.array(itemSchema),
    pagination: z.object({
      page: z.number(),
      pageSize: z.number(),
      totalPages: z.number(),
      totalItems: z.number(),
      hasMore: z.boolean(),
    }),
  });

export type User = z.infer<typeof userSchema>;
export type PaginatedResponse<T> = {
  data: T[];
  pagination: {
    page: number;
    pageSize: number;
    totalPages: number;
    totalItems: number;
    hasMore: boolean;
  };
};

// Request types
export interface RequestConfig {
  timeout?: number;
  retries?: number;
  headers?: Record<string, string>;
}

export interface PaginationParams {
  page?: number;
  pageSize?: number;
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
}

// Client configuration
export interface ClientConfig {
  apiKey: string;
  baseUrl?: string;
  timeout?: number;
  maxRetries?: number;
  debug?: boolean;
  onRequest?: (request: Request) => Request | Promise<Request>;
  onResponse?: (response: Response) => Response | Promise<Response>;
  onError?: (error: Error) => void;
}

Custom Errors

// src/errors/index.ts
export class ApiError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number,
    public readonly code: string,
    public readonly details?: unknown
  ) {
    super(message);
    this.name = 'ApiError';
  }

  static fromResponse(response: Response, body: unknown): ApiError {
    const details = body as { message?: string; code?: string; details?: unknown };
    return new ApiError(
      details?.message || response.statusText,
      response.status,
      details?.code || 'UNKNOWN_ERROR',
      details?.details
    );
  }
}

export class AuthenticationError extends ApiError {
  constructor(message = 'Authentication failed') {
    super(message, 401, 'AUTHENTICATION_ERROR');
    this.name = 'AuthenticationError';
  }
}

export class RateLimitError extends ApiError {
  constructor(
    public readonly retryAfter: number,
    message = 'Rate limit exceeded'
  ) {
    super(message, 429, 'RATE_LIMIT_ERROR');
    this.name = 'RateLimitError';
  }
}

export class ValidationError extends ApiError {
  constructor(message: string, details?: unknown) {
    super(message, 400, 'VALIDATION_ERROR', details);
    this.name = 'ValidationError';
  }
}

export class NotFoundError extends ApiError {
  constructor(resource: string) {
    super(`${resource} not found`, 404, 'NOT_FOUND');
    this.name = 'NotFoundError';
  }
}

HTTP Client Core

// src/client/http.ts
import {
  ApiError,
  AuthenticationError,
  RateLimitError,
} from '../errors/index.js';
import type { ClientConfig, RequestConfig } from '../types/index.js';

export class HttpClient {
  private config: Required<Omit<ClientConfig, 'onRequest' | 'onResponse' | 'onError'>> &
    Pick<ClientConfig, 'onRequest' | 'onResponse' | 'onError'>;

  constructor(config: ClientConfig) {
    this.config = {
      apiKey: config.apiKey,
      baseUrl: config.baseUrl || 'https://api.example.com/v1',
      timeout: config.timeout || 30000,
      maxRetries: config.maxRetries || 3,
      debug: config.debug || false,
      onRequest: config.onRequest,
      onResponse: config.onResponse,
      onError: config.onError,
    };
  }

  async request<T>(
    method: string,
    path: string,
    options: {
      body?: unknown;
      params?: Record<string, string | number | boolean | undefined>;
      config?: RequestConfig;
    } = {}
  ): Promise<T> {
    const url = this.buildUrl(path, options.params);
    const retries = options.config?.retries ?? this.config.maxRetries;

    let lastError: Error | null = null;

    for (let attempt = 0; attempt <= retries; attempt++) {
      try {
        const response = await this.executeRequest(method, url, options);
        return response as T;
      } catch (error) {
        lastError = error as Error;

        // Don't retry on auth errors or validation errors
        if (
          error instanceof AuthenticationError ||
          error instanceof ApiError && error.statusCode < 500
        ) {
          throw error;
        }

        // Handle rate limiting
        if (error instanceof RateLimitError) {
          if (attempt < retries) {
            await this.delay(error.retryAfter * 1000);
            continue;
          }
        }

        // Exponential backoff for other errors
        if (attempt < retries) {
          const backoff = Math.min(1000 * Math.pow(2, attempt), 10000);
          this.log(`Retry ${attempt + 1}/${retries} after ${backoff}ms`);
          await this.delay(backoff);
        }
      }
    }

    this.config.onError?.(lastError!);
    throw lastError;
  }

  private async executeRequest(
    method: string,
    url: URL,
    options: { body?: unknown; config?: RequestConfig }
  ): Promise<unknown> {
    const headers = new Headers({
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${this.config.apiKey}`,
      ...options.config?.headers,
    });

    let request = new Request(url.toString(), {
      method,
      headers,
      body: options.body ? JSON.stringify(options.body) : undefined,
    });

    // Apply request interceptor
    if (this.config.onRequest) {
      request = await this.config.onRequest(request);
    }

    const controller = new AbortController();
    const timeout = options.config?.timeout ?? this.config.timeout;
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      this.log(`${method} ${url.pathname}`);

      let response = await fetch(request, { signal: controller.signal });

      // Apply response interceptor
      if (this.config.onResponse) {
        response = await this.config.onResponse(response);
      }

      const body = await response.json().catch(() => null);

      if (!response.ok) {
        this.handleErrorResponse(response, body);
      }

      return body;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  private handleErrorResponse(response: Response, body: unknown): never {
    if (response.status === 401) {
      throw new AuthenticationError();
    }

    if (response.status === 429) {
      const retryAfter = parseInt(
        response.headers.get('Retry-After') || '60',
        10
      );
      throw new RateLimitError(retryAfter);
    }

    throw ApiError.fromResponse(response, body);
  }

  private buildUrl(
    path: string,
    params?: Record<string, string | number | boolean | undefined>
  ): URL {
    const url = new URL(path, this.config.baseUrl);

    if (params) {
      Object.entries(params).forEach(([key, value]) => {
        if (value !== undefined) {
          url.searchParams.set(key, String(value));
        }
      });
    }

    return url;
  }

  private delay(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  private log(message: string): void {
    if (this.config.debug) {
      console.log(`[API Client] ${message}`);
    }
  }

  // Convenience methods
  get<T>(path: string, params?: Record<string, string | number | boolean | undefined>, config?: RequestConfig) {
    return this.request<T>('GET', path, { params, config });
  }

  post<T>(path: string, body?: unknown, config?: RequestConfig) {
    return this.request<T>('POST', path, { body, config });
  }

  put<T>(path: string, body?: unknown, config?: RequestConfig) {
    return this.request<T>('PUT', path, { body, config });
  }

  patch<T>(path: string, body?: unknown, config?: RequestConfig) {
    return this.request<T>('PATCH', path, { body, config });
  }

  delete<T>(path: string, config?: RequestConfig) {
    return this.request<T>('DELETE', path, { config });
  }
}

Resource Classes

// src/resources/users.ts
import type { HttpClient } from '../client/http.js';
import { userSchema, paginatedResponseSchema } from '../types/index.js';
import type { User, PaginatedResponse, PaginationParams } from '../types/index.js';

export class UsersResource {
  constructor(private http: HttpClient) {}

  async list(params?: PaginationParams): Promise<PaginatedResponse<User>> {
    const response = await this.http.get<unknown>('/users', params);
    return paginatedResponseSchema(userSchema).parse(response);
  }

  async get(id: string): Promise<User> {
    const response = await this.http.get<unknown>(`/users/${id}`);
    return userSchema.parse(response);
  }

  async create(data: {
    email: string;
    name: string;
    password: string;
  }): Promise<User> {
    const response = await this.http.post<unknown>('/users', data);
    return userSchema.parse(response);
  }

  async update(
    id: string,
    data: Partial<{ email: string; name: string }>
  ): Promise<User> {
    const response = await this.http.patch<unknown>(`/users/${id}`, data);
    return userSchema.parse(response);
  }

  async delete(id: string): Promise<void> {
    await this.http.delete(`/users/${id}`);
  }

  // Pagination helper
  async *listAll(pageSize = 100): AsyncGenerator<User, void, unknown> {
    let page = 1;
    let hasMore = true;

    while (hasMore) {
      const response = await this.list({ page, pageSize });
      for (const user of response.data) {
        yield user;
      }
      hasMore = response.pagination.hasMore;
      page++;
    }
  }
}

Main Client Class

// src/client/index.ts
import { HttpClient } from './http.js';
import { UsersResource } from '../resources/users.js';
import type { ClientConfig } from '../types/index.js';

export class ApiClient {
  private http: HttpClient;

  // Resources
  public readonly users: UsersResource;

  constructor(config: ClientConfig) {
    this.http = new HttpClient(config);

    // Initialize resources
    this.users = new UsersResource(this.http);
  }

  // Factory method for convenience
  static create(apiKey: string, options?: Omit<ClientConfig, 'apiKey'>) {
    return new ApiClient({ apiKey, ...options });
  }
}

// Re-export types and errors
export * from '../types/index.js';
export * from '../errors/index.js';

Public Exports

// src/index.ts
export { ApiClient } from './client/index.js';
export type {
  User,
  PaginatedResponse,
  PaginationParams,
  ClientConfig,
  RequestConfig,
} from './types/index.js';
export {
  ApiError,
  AuthenticationError,
  RateLimitError,
  ValidationError,
  NotFoundError,
} from './errors/index.js';

Usage Examples

// examples/basic-usage.ts
import { ApiClient, ApiError, RateLimitError } from '@yourorg/api-client';

// Initialize client
const client = ApiClient.create('your-api-key', {
  debug: true,
  maxRetries: 3,
});

// Basic CRUD operations
async function main() {
  try {
    // List users with pagination
    const { data: users, pagination } = await client.users.list({
      page: 1,
      pageSize: 10,
    });

    console.log(`Found ${pagination.totalItems} users`);

    // Get single user
    const user = await client.users.get('user-123');
    console.log(`User: ${user.name}`);

    // Create user
    const newUser = await client.users.create({
      email: 'new@example.com',
      name: 'New User',
      password: 'securepassword',
    });

    // Update user
    await client.users.update(newUser.id, { name: 'Updated Name' });

    // Iterate all users (handles pagination automatically)
    for await (const user of client.users.listAll()) {
      console.log(user.email);
    }
  } catch (error) {
    if (error instanceof RateLimitError) {
      console.log(`Rate limited. Retry after ${error.retryAfter} seconds`);
    } else if (error instanceof ApiError) {
      console.error(`API Error: ${error.message} (${error.code})`);
    } else {
      throw error;
    }
  }
}

main();

Testing

// src/__tests__/client.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ApiClient, AuthenticationError, RateLimitError } from '../index.js';

describe('ApiClient', () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  it('creates client with API key', () => {
    const client = ApiClient.create('test-key');
    expect(client).toBeDefined();
    expect(client.users).toBeDefined();
  });

  it('handles authentication errors', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 401,
      json: () => Promise.resolve({ message: 'Invalid API key' }),
    });

    const client = ApiClient.create('invalid-key');

    await expect(client.users.list()).rejects.toThrow(AuthenticationError);
  });

  it('handles rate limiting with retry', async () => {
    let attempts = 0;
    global.fetch = vi.fn().mockImplementation(() => {
      attempts++;
      if (attempts === 1) {
        return Promise.resolve({
          ok: false,
          status: 429,
          headers: new Headers({ 'Retry-After': '1' }),
          json: () => Promise.resolve({}),
        });
      }
      return Promise.resolve({
        ok: true,
        json: () =>
          Promise.resolve({
            data: [],
            pagination: { page: 1, pageSize: 10, totalPages: 0, totalItems: 0, hasMore: false },
          }),
      });
    });

    const client = ApiClient.create('test-key', { maxRetries: 2 });
    await client.users.list();

    expect(attempts).toBe(2);
  });
});

Key Takeaways

  1. Zod validation ensures runtime type safety for API responses
  2. Error classification helps consumers handle different failure modes
  3. Automatic retries with exponential backoff improve reliability
  4. AsyncGenerator simplifies pagination consumption
  5. Request interceptors enable customization without modifying core logic

بناء مكتبات عملاء API

هدف المشروع: إنشاء مكتبة عميل API احترافية سيحب المطورون الآخرون استخدامها.

لماذا نبني مكتبات عملاء API؟

عملاء API قيّمون لأنهم:

  • يحلون مشاكل حقيقية - يجعلون تكامل API بلا جهد
  • إعادة استخدام عالية - تُستخدم عبر مشاريع كثيرة
  • صديقة لـ TypeScript - الأنواع تحسن تجربة المطور بشكل كبير
  • توزيع npm - سهلة المشاركة والإصدار

برومبت مشروع عميل API

أنشئ مكتبة عميل API بـ TypeScript لـ [اسم API]:

## الميزات
- معالجة طلب/استجابة آمنة الأنواع
- إعادة محاولة تلقائية مع تراجع أسي
- دعم تحديد المعدل
- اعتراضات الطلب/الاستجابة
- تصنيف الأخطاء
- معالجة المصادقة (مفتاح API، OAuth، إلخ)
- مساعدات التصفح بالصفحات
- خيارات تسجيل شاملة

## التقنيات
- TypeScript مع الوضع الصارم
- fetch أو axios لـ HTTP
- Zod للتحقق في وقت التشغيل
- tsup للبناء
- Vitest للاختبار

## هيكل المشروع

/src /client # فئة العميل الرئيسية /resources # فئات موارد API /types # أنواع TypeScript /utils # أدوات (إعادة المحاولة، تحديد المعدل) /errors # فئات الأخطاء المخصصة index.ts # الصادرات العامة /examples # أمثلة الاستخدام


## نقاط نهاية API المدعومة
[اذكر نقاط نهاية API الرئيسية]

## طريقة المصادقة
[صف المصادقة: مفتاح API، OAuth، رمز Bearer، إلخ]

تعريفات الأنواع

// src/types/index.ts
import { z } from 'zod';

// مخططات استجابة API
export const userSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  name: z.string(),
  avatar: z.string().url().optional(),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
});

export const paginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
  z.object({
    data: z.array(itemSchema),
    pagination: z.object({
      page: z.number(),
      pageSize: z.number(),
      totalPages: z.number(),
      totalItems: z.number(),
      hasMore: z.boolean(),
    }),
  });

export type User = z.infer<typeof userSchema>;
export type PaginatedResponse<T> = {
  data: T[];
  pagination: {
    page: number;
    pageSize: number;
    totalPages: number;
    totalItems: number;
    hasMore: boolean;
  };
};

الأخطاء المخصصة

// src/errors/index.ts
export class ApiError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number,
    public readonly code: string,
    public readonly details?: unknown
  ) {
    super(message);
    this.name = 'ApiError';
  }

  static fromResponse(response: Response, body: unknown): ApiError {
    const details = body as { message?: string; code?: string; details?: unknown };
    return new ApiError(
      details?.message || response.statusText,
      response.status,
      details?.code || 'UNKNOWN_ERROR',
      details?.details
    );
  }
}

export class AuthenticationError extends ApiError {
  constructor(message = 'فشلت المصادقة') {
    super(message, 401, 'AUTHENTICATION_ERROR');
    this.name = 'AuthenticationError';
  }
}

export class RateLimitError extends ApiError {
  constructor(
    public readonly retryAfter: number,
    message = 'تم تجاوز حد المعدل'
  ) {
    super(message, 429, 'RATE_LIMIT_ERROR');
    this.name = 'RateLimitError';
  }
}

فئة العميل الرئيسية

// src/client/index.ts
import { HttpClient } from './http.js';
import { UsersResource } from '../resources/users.js';
import type { ClientConfig } from '../types/index.js';

export class ApiClient {
  private http: HttpClient;

  // الموارد
  public readonly users: UsersResource;

  constructor(config: ClientConfig) {
    this.http = new HttpClient(config);

    // تهيئة الموارد
    this.users = new UsersResource(this.http);
  }

  // طريقة المصنع للراحة
  static create(apiKey: string, options?: Omit<ClientConfig, 'apiKey'>) {
    return new ApiClient({ apiKey, ...options });
  }
}

// إعادة تصدير الأنواع والأخطاء
export * from '../types/index.js';
export * from '../errors/index.js';

أمثلة الاستخدام

// examples/basic-usage.ts
import { ApiClient, ApiError, RateLimitError } from '@yourorg/api-client';

// تهيئة العميل
const client = ApiClient.create('your-api-key', {
  debug: true,
  maxRetries: 3,
});

// عمليات CRUD الأساسية
async function main() {
  try {
    // قائمة المستخدمين مع التصفح
    const { data: users, pagination } = await client.users.list({
      page: 1,
      pageSize: 10,
    });

    console.log(`وُجد ${pagination.totalItems} مستخدم`);

    // الحصول على مستخدم واحد
    const user = await client.users.get('user-123');
    console.log(`المستخدم: ${user.name}`);

    // إنشاء مستخدم
    const newUser = await client.users.create({
      email: 'new@example.com',
      name: 'مستخدم جديد',
      password: 'كلمة_مرور_آمنة',
    });

    // تحديث مستخدم
    await client.users.update(newUser.id, { name: 'الاسم المحدث' });

    // التكرار على جميع المستخدمين (يتعامل مع التصفح تلقائياً)
    for await (const user of client.users.listAll()) {
      console.log(user.email);
    }
  } catch (error) {
    if (error instanceof RateLimitError) {
      console.log(`تم تحديد المعدل. أعد المحاولة بعد ${error.retryAfter} ثانية`);
    } else if (error instanceof ApiError) {
      console.error(`خطأ API: ${error.message} (${error.code})`);
    } else {
      throw error;
    }
  }
}

main();

الاختبار

// src/__tests__/client.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ApiClient, AuthenticationError, RateLimitError } from '../index.js';

describe('ApiClient', () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  it('ينشئ عميل بمفتاح API', () => {
    const client = ApiClient.create('test-key');
    expect(client).toBeDefined();
    expect(client.users).toBeDefined();
  });

  it('يتعامل مع أخطاء المصادقة', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 401,
      json: () => Promise.resolve({ message: 'مفتاح API غير صالح' }),
    });

    const client = ApiClient.create('invalid-key');

    await expect(client.users.list()).rejects.toThrow(AuthenticationError);
  });

  it('يتعامل مع تحديد المعدل مع إعادة المحاولة', async () => {
    let attempts = 0;
    global.fetch = vi.fn().mockImplementation(() => {
      attempts++;
      if (attempts === 1) {
        return Promise.resolve({
          ok: false,
          status: 429,
          headers: new Headers({ 'Retry-After': '1' }),
          json: () => Promise.resolve({}),
        });
      }
      return Promise.resolve({
        ok: true,
        json: () =>
          Promise.resolve({
            data: [],
            pagination: { page: 1, pageSize: 10, totalPages: 0, totalItems: 0, hasMore: false },
          }),
      });
    });

    const client = ApiClient.create('test-key', { maxRetries: 2 });
    await client.users.list();

    expect(attempts).toBe(2);
  });
});

النقاط الرئيسية

  1. تحقق Zod يضمن سلامة الأنواع في وقت التشغيل لاستجابات API
  2. تصنيف الأخطاء يساعد المستهلكين على التعامل مع أوضاع الفشل المختلفة
  3. إعادة المحاولات التلقائية مع التراجع الأسي تحسن الموثوقية
  4. AsyncGenerator يبسط استهلاك التصفح بالصفحات
  5. اعتراضات الطلب تُمكّن التخصيص بدون تعديل المنطق الأساسي

اختبار

الوحدة 3: بناء أدوات المطورين

خذ الاختبار