Skip to content

Instantly share code, notes, and snippets.

@sschepis
Last active September 12, 2024 18:07
Show Gist options
  • Save sschepis/45e904a9391b669b27106883afc749ea to your computer and use it in GitHub Desktop.
Save sschepis/45e904a9391b669b27106883afc749ea to your computer and use it in GitHub Desktop.
what does this code do:
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { RateLimiter } from 'limiter';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
interface EndpointConfig<T extends Record<string, any> = Record<string, any>> {
method: HttpMethod;
path: string;
params?: (keyof T)[];
headers?: Record<string, string>;
responseType?: AxiosRequestConfig['responseType'];
rateLimitPerSecond?: number;
retryConfig?: RetryConfig;
}
interface RetryConfig {
maxRetries: number;
retryDelay: number;
retryCondition?: (error: AxiosError) => boolean;
}
interface ApiConfig {
baseUrl: string;
endpoints: Record<string, EndpointConfig>;
globalHeaders?: Record<string, string>;
timeout?: number;
responseInterceptor?: (response: AxiosResponse) => AxiosResponse | Promise<AxiosResponse>;
errorInterceptor?: (error: any) => any;
globalRetryConfig?: RetryConfig;
queueConcurrency?: number;
}
type ApiResponse<T> = Promise<T>;
class RequestQueue {
private queue: (() => Promise<any>)[] = [];
private running = 0;
constructor(private concurrency: number) {}
enqueue<T>(task: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push(() => task().then(resolve).catch(reject));
this.runNext();
});
}
private runNext() {
if (this.running >= this.concurrency || this.queue.length === 0) return;
this.running++;
const task = this.queue.shift();
if (task) {
task().finally(() => {
this.running--;
this.runNext();
});
}
}
}
class DynamicApi {
private axiosInstance: AxiosInstance;
private config: ApiConfig;
private rateLimiters: Map<string, RateLimiter> = new Map();
private requestQueue: RequestQueue;
constructor(config: ApiConfig) {
this.config = config;
this.axiosInstance = axios.create({
baseURL: config.baseUrl,
timeout: config.timeout || 10000,
headers: config.globalHeaders || {},
});
if (config.responseInterceptor) {
this.axiosInstance.interceptors.response.use(config.responseInterceptor);
}
if (config.errorInterceptor) {
this.axiosInstance.interceptors.response.use(undefined, config.errorInterceptor);
}
this.requestQueue = new RequestQueue(config.queueConcurrency || 5);
}
createApiMethods<T extends Record<string, EndpointConfig>>() {
const api: Record<string, Function> = {};
for (const [name, endpoint] of Object.entries(this.config.endpoints)) {
api[name] = this.createMethod(name, endpoint);
if (endpoint.rateLimitPerSecond) {
this.rateLimiters.set(name, new RateLimiter({ tokensPerInterval: endpoint.rateLimitPerSecond, interval: 'second' }));
}
}
return api as {
[K in keyof T]: (
data?: { [P in T[K]['params'][number]]?: any } & Record<string, any>,
config?: AxiosRequestConfig
) => ApiResponse<any>
};
}
private createMethod<T extends Record<string, any>>(name: string, endpoint: EndpointConfig<T>) {
return async (data: T = {} as T, config: AxiosRequestConfig = {}): ApiResponse<any> => {
const rateLimiter = this.rateLimiters.get(name);
if (rateLimiter) {
await rateLimiter.removeTokens(1);
}
return this.requestQueue.enqueue(() => this.makeRequest(endpoint, data, config));
};
}
private async makeRequest<T extends Record<string, any>>(
endpoint: EndpointConfig<T>,
data: T,
config: AxiosRequestConfig
): Promise<any> {
const { method, path, params = [], headers = {}, responseType, retryConfig } = endpoint;
let url = path;
// Replace path parameters
for (const param of params) {
if (data[param as string] !== undefined) {
url = url.replace(`:${param}`, encodeURIComponent(data[param as string]));
delete data[param as string];
}
}
const axiosConfig: AxiosRequestConfig = {
...config,
method,
url,
headers: { ...headers, ...config.headers },
responseType,
};
if (['POST', 'PUT', 'PATCH'].includes(method)) {
axiosConfig.data = data;
} else {
axiosConfig.params = data;
}
const finalRetryConfig = { ...this.config.globalRetryConfig, ...retryConfig };
return this.retryRequest(axiosConfig, finalRetryConfig);
}
private async retryRequest(
config: AxiosRequestConfig,
retryConfig: RetryConfig
): Promise<any> {
const { maxRetries, retryDelay, retryCondition } = retryConfig;
let retries = 0;
while (true) {
try {
const response = await this.axiosInstance.request(config);
return response.data;
} catch (error) {
if (
axios.isAxiosError(error) &&
retries < maxRetries &&
(!retryCondition || retryCondition(error))
) {
retries++;
await new Promise(resolve => setTimeout(resolve, retryDelay));
continue;
}
throw error;
}
}
}
updateGlobalHeaders(headers: Record<string, string>) {
this.axiosInstance.defaults.headers.common = {
...this.axiosInstance.defaults.headers.common,
...headers,
};
}
setAuthToken(token: string) {
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
}
// Usage example
interface User {
id: number;
name: string;
email: string;
}
const apiConfig: ApiConfig = {
baseUrl: 'https://api.example.com',
endpoints: {
getUsers: {
method: 'GET',
path: '/users',
rateLimitPerSecond: 5,
retryConfig: { maxRetries: 3, retryDelay: 1000 }
},
getUser: {
method: 'GET',
path: '/users/:id',
params: ['id'],
retryConfig: { maxRetries: 2, retryDelay: 500 }
},
createUser: { method: 'POST', path: '/users' },
updateUser: { method: 'PUT', path: '/users/:id', params: ['id'] },
deleteUser: { method: 'DELETE', path: '/users/:id', params: ['id'] },
},
globalHeaders: {
'Content-Type': 'application/json',
},
timeout: 5000,
responseInterceptor: (response) => {
console.log('Response:', response.data);
return response;
},
errorInterceptor: (error) => {
console.error('Request failed:', error);
return Promise.reject(error);
},
globalRetryConfig: {
maxRetries: 3,
retryDelay: 1000,
retryCondition: (error: AxiosError) => error.response?.status === 429
},
queueConcurrency: 3
};
const dynamicApi = new DynamicApi(apiConfig);
const api = dynamicApi.createApiMethods<typeof apiConfig.endpoints>();
// Example usage
(async () => {
try {
const users = await api.getUsers();
console.log('Users:', users);
const user = await api.getUser({ id: 1 });
console.log('User:', user);
const newUser = await api.createUser({ name: 'John Doe', email: 'john@example.com' });
console.log('New user:', newUser);
const updatedUser = await api.updateUser({ id: 1, name: 'Jane Doe' });
console.log('Updated user:', updatedUser);
await api.deleteUser({ id: 1 });
console.log('User deleted');
dynamicApi.updateGlobalHeaders({ 'X-Custom-Header': 'value' });
dynamicApi.setAuthToken('your-auth-token');
} catch (error) {
console.error('Error:', error);
}
})();
@sschepis
Copy link
Author

Dynamic API Client with Rate Limiting, Retry, and Queueing

This library provides a dynamic and flexible API client with features like rate limiting, retrying failed requests, and queueing requests to manage concurrency.

Features:

  • Dynamic API Definition: Define your API endpoints and their configurations in a single object.
  • Rate Limiting: Limit requests per second to prevent hitting API rate limits.
  • Retry Mechanism: Retry failed requests with configurable retry attempts and delays.
  • Request Queueing: Manage concurrency by queueing requests and running them concurrently based on a configurable limit.
  • Interceptors: Intercept responses and errors for custom handling.
  • Global Headers and Authentication: Set global headers and authentication tokens easily.

Installation:

npm install

Usage:

  1. Define API Configuration:

    const apiConfig = {
      baseUrl: 'https://api.example.com',
      endpoints: {
        getUsers: {
          method: 'GET',
          path: '/users',
          rateLimitPerSecond: 5,
          retryConfig: { maxRetries: 3, retryDelay: 1000 }
        },
        // ... other endpoints
      },
      // ... other configuration options
    };
  2. Create Dynamic API Instance:

    const dynamicApi = new DynamicApi(apiConfig);
  3. Generate API Methods:

    const api = dynamicApi.createApiMethods<typeof apiConfig.endpoints>();
  4. Call API Methods:

    const users = await api.getUsers();
    console.log('Users:', users);

Example:

// Usage example
interface User {
  id: number;
  name: string;
  email: string;
}

const apiConfig: ApiConfig = {
  baseUrl: 'https://api.example.com',
  endpoints: {
    getUsers: {
      method: 'GET',
      path: '/users',
      rateLimitPerSecond: 5,
      retryConfig: { maxRetries: 3, retryDelay: 1000 }
    },
    getUser: {
      method: 'GET',
      path: '/users/:id',
      params: ['id'],
      retryConfig: { maxRetries: 2, retryDelay: 500 }
    },
    createUser: { method: 'POST', path: '/users' },
    updateUser: { method: 'PUT', path: '/users/:id', params: ['id'] },
    deleteUser: { method: 'DELETE', path: '/users/:id', params: ['id'] },
  },
  globalHeaders: {
    'Content-Type': 'application/json',
  },
  timeout: 5000,
  responseInterceptor: (response) => {
    console.log('Response:', response.data);
    return response;
  },
  errorInterceptor: (error) => {
    console.error('Request failed:', error);
    return Promise.reject(error);
  },
  globalRetryConfig: {
    maxRetries: 3,
    retryDelay: 1000,
    retryCondition: (error: AxiosError) => error.response?.status === 429
  },
  queueConcurrency: 3
};

const dynamicApi = new DynamicApi(apiConfig);
const api = dynamicApi.createApiMethods<typeof apiConfig.endpoints>();

// Example usage
(async () => {
  try {
    const users = await api.getUsers();
    console.log('Users:', users);

    const user = await api.getUser({ id: 1 });
    console.log('User:', user);

    const newUser = await api.createUser({ name: 'John Doe', email: 'john@example.com' });
    console.log('New user:', newUser);

    const updatedUser = await api.updateUser({ id: 1, name: 'Jane Doe' });
    console.log('Updated user:', updatedUser);

    await api.deleteUser({ id: 1 });
    console.log('User deleted');

    dynamicApi.updateGlobalHeaders({ 'X-Custom-Header': 'value' });
    dynamicApi.setAuthToken('your-auth-token');

  } catch (error) {
    console.error('Error:', error);
  }
})();

Configuration:

  • baseUrl: The base URL of the API.
  • endpoints: An object defining each endpoint configuration.
    • method: The HTTP method (GET, POST, PUT, DELETE, PATCH).
    • path: The path of the endpoint.
    • params: An array of parameter names.
    • headers: Custom headers for this endpoint.
    • responseType: The desired response type.
    • rateLimitPerSecond: The number of requests allowed per second.
    • retryConfig: Configuration for retrying requests.
      • maxRetries: The maximum number of retries.
      • retryDelay: The delay in milliseconds between retries.
      • retryCondition: A function to check if a request should be retried.
  • globalHeaders: Global headers to apply to all requests.
  • timeout: The request timeout in milliseconds.
  • responseInterceptor: A function to intercept and modify responses.
  • errorInterceptor: A function to intercept and handle errors.
  • globalRetryConfig: Global retry configuration for all endpoints.
  • queueConcurrency: The maximum number of requests to run concurrently.

Note:

This library is built on top of Axios. You can customize its functionality by using Axios interceptors and configuration options.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment