- π― Dynamic Routes -
api.users.getProfile(),api.admin.users.ban() - π Parameterized Endpoints -
api.users(123).follow(),api.posts('slug').view() - π§ TypeScript First - Full type safety and IntelliSense support
- π Modern Fetch API - No XMLHttpRequest, pure modern JavaScript
- π Interceptors - Request/Response middleware with logging, caching, performance
- β‘ Automatic Retries - Configurable retry logic with exponential backoff
- π·οΈ Case Conversion - Automatic camelCase β kebab-case conversion
- π File Upload - Automatic FormData handling for File/Blob objects
- π¨ Flexible Configuration - Per-request and global settings
npm install @metis-w/api-clientimport { APIClient } from '@metis-w/api-client';
const api = new APIClient({
baseUrl: 'https://api.example.com',
timeout: 5000
});
// Simple requests
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'John', email: 'john@example.com' });
// Alternative: Use convenience functions
import { createClient, createDynamicClient, IDynamicClient } from '@metis-w/api-client';
const apiClient = createClient({ baseUrl: 'https://api.example.com' });
const dynamicApi: IDynamicClient = createDynamicClient({ baseUrl: 'https://api.example.com' });import { DynamicClient } from '@metis-w/api-client/core';
import { CacheInterceptor } from '@metis-w/api-client/interceptors';import { DynamicClient, IDynamicClient } from '@metis-w/api-client';
const api: IDynamicClient = new DynamicClient({
baseUrl: 'https://api.example.com',
useKebabCase: true // converts getUserInfo β get-user-info
});
// Dynamic routing magic - no 'as any' needed!
const profile = await api.users.getProfile({ id: 123 });
const result = await api.admin.users.ban({ userId: 456, reason: 'spam' });
// Multi-level routes
const settings = await api.users.profile.getSettings({ theme: 'dark' });Enhanced parameterized routes with direct call support:
// Traditional parameterized routes with actions (creates nested paths)
const userProfile = await api.users(123).getProfile(); // GET /users/123/getProfile
const follow = await api.users(123).follow({ notify: true }); // POST /users/123/follow
const profile = await api.users(456).profile.update({ bio: 'New bio' }); // PUT /users/456/profile/update
// NEW: Direct parameterized route calls (RESTful) - URL will reflect useKebabCase if enabled (e.g. getProfile β get-profile)
const user = await api.users(123)(); // GET /users/123
const updated = await api.users(123)({ name: "John" }); // PUT /users/123 (with payload = update)
const deleted = await api.users(123)({ method: "DELETE" }); // DELETE /users/123 (explicit method)
// With query parameters
const user = await api.users(123)({}, { include: "profile" }); // GET /users/123?include=profile
const updated = await api.users(123)({ name: "John" }, { format: "json" }); // PUT /users/123?format=json- No payload:
GET /resource/id- Retrieve the resource - With payload:
PUT /resource/id- Update the resource (default semantic) - Explicit method: Override with
{ method: "DELETE" }etc. - Query params: Second parameter for URL query string
The DynamicClient intelligently determines the appropriate HTTP method based on several factors:
// Override any automatic method detection
await api.users.create({ name: "John", method: "PUT" }); // Forces PUT method
await api.users.delete({ confirm: true, method: "POST" }); // Forces POST method// Direct method calls
await api.users.get(); // GET /users/get
await api.users.post(); // POST /users/post
await api.users.put(); // PUT /users/put
await api.users.delete(); // DELETE /users/delete
await api.users.patch(); // PATCH /users/patchThe library automatically detects the intent from action names:
// GET methods (reading data)
await api.users.fetch(); // GET
await api.users.load(); // GET
await api.users.find(); // GET
await api.users.retrieve(); // GET
await api.users.show(); // GET
await api.users.view(); // GET
await api.users.getProfile(); // GET (starts with 'get')
await api.users.loadSettings(); // GET (starts with 'load')
// POST methods (creating data)
await api.users.create(); // POST
await api.users.add(); // POST
await api.users.save(); // POST
await api.users.store(); // POST
await api.users.insert(); // POST
await api.users.register(); // POST
await api.users.submit(); // POST
await api.users.createUser(); // POST (starts with 'create')
// PUT methods (updating/replacing data)
await api.users.update(); // PUT
await api.users.replace(); // PUT
await api.users.modify(); // PUT
await api.users.edit(); // PUT
await api.users.change(); // PUT
await api.users.set(); // PUT
await api.users.updateProfile(); // PUT (starts with 'update')
// DELETE methods (removing data)
await api.users.delete(); // DELETE
await api.users.remove(); // DELETE
await api.users.destroy(); // DELETE
await api.users.clear(); // DELETE
await api.users.drop(); // DELETE
await api.users.deleteUser(); // DELETE (starts with 'delete')
// PATCH methods (partial updates)
await api.users.patch(); // PATCH
await api.users.partial(); // PATCH
await api.users.toggle(); // PATCH
await api.users.enable(); // PATCH
await api.users.disable(); // PATCH
await api.users.activate(); // PATCH
await api.users.deactivate(); // PATCHDefine custom patterns for your API. Patterns are matched against action names (not controller names), case-insensitively. Matching supports prefix/suffix wildcards and kebab-case or camelCase action names:
const api = new DynamicClient({
baseUrl: 'https://api.example.com',
methodRules: {
'validate*': 'POST', // e.g., validateInput β POST
'*report': 'GET', // e.g., monthlyReport β GET
'verify*': 'GET', // e.g., verifyToken β GET
'delete*': 'DELETE' // e.g., deleteUser β DELETE
}
});
await api.data.validateInput(); // POST (pattern match)
await api.sales.monthlyReport(); // GET (pattern match)
await api.auth.verifyToken(); // GET (pattern match)Set a fallback method for unrecognized actions:
const api = new DynamicClient({
baseUrl: 'https://api.example.com',
defaultMethod: 'GET' // Default to GET instead of POST
});
await api.users.unknownAction(); // GET (fallback to default)The method resolution follows this priority:
- Explicit method in payload (
{ method: "DELETE" }) - Direct HTTP method (
get,post,put,delete,patch) - Custom method rules (from
methodRulesconfig) - Semantic analysis (action name patterns)
- Default method (from
defaultMethodconfig, defaults toPOST)
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const file = fileInput.files?.[0];
if (file) {
// Automatic FormData handling
const response = await api.post('/upload/avatar', {
file: file,
userId: 123,
metadata: { title: 'Profile Picture' }
});
}
// Multiple files
const files = Array.from(fileInput.files || []);
const response = await api.post('/upload/gallery', {
files: files,
albumId: 456,
tags: ['vacation', 'summer']
});Full TypeScript integration with intelligent type inference and strict settings. Public APIs are generic-first; a few internal defaults use any for flexibility.
// Import types for proper typing
import { DynamicClient, IDynamicClient, APIClient } from '@metis-w/api-client';
// Define your API response types
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
interface CreateUserRequest {
name: string;
email: string;
password: string;
}
// Type-safe API calls with APIClient
const client = new APIClient({ baseUrl: 'https://api.example.com' });
const user = await client.get<User>('/users/123');
// user.data is typed as User | undefined
const newUser = await client.post<User, CreateUserRequest>('/users', {
name: 'John Doe',
email: 'john@example.com',
password: 'secure123'
});
// Type-safe dynamic client - no 'as any' casting needed!
const dynamicClient: IDynamicClient = new DynamicClient({
baseUrl: 'https://api.example.com',
defaultMethod: 'GET',
methodRules: {
'auth*': 'POST'
}
});
// TypeScript understands these are dynamic routes with proper return types
const userData = await dynamicClient.users.getProfile({ id: 123 });
// userData is typed as APIResponse<unknown>
// All HTTP methods work seamlessly
await dynamicClient.users.create({ name: "John" }); // POST (semantic)
await dynamicClient.users.update({ id: 1 }); // PUT (semantic)
await dynamicClient.users.fetch(); // GET (semantic)
await dynamicClient.users.remove(); // DELETE (semantic)
await dynamicClient.users.patch({ status: "active" }); // PATCH (semantic)
// Custom method rules
await dynamicClient.auth.login({ email: "test@test.com" }); // POST (rule)
// Explicit method override
await dynamicClient.users.action({ data: "test", method: "DELETE" }); // DELETE (explicit)
// RESTful parameterized routes
const user = await dynamicClient.users(123)(); // GET /users/123
const updated = await dynamicClient.users(123)({ name: 'John Smith' }); // PUT /users/123
// Traditional action-based parameterized routes
const profile = await dynamicClient.users(123).getProfile(); // GET /users/123/getProfile
const follow = await dynamicClient.users(123).follow(); // POST /users/123/follow// Create a typed API client
class TypedAPIClient extends APIClient {
async getUser(id: number): Promise<User> {
const response = await this.get<User>(`/users/${id}`);
if (!response.success || !response.data) {
throw new Error(response.error?.message || 'User not found');
}
return response.data;
}
async createUser(userData: CreateUserRequest): Promise<User> {
const response = await this.post<User, CreateUserRequest>('/users', userData);
if (!response.success || !response.data) {
throw new Error(response.error?.message || 'Failed to create user');
}
return response.data;
}
}
const typedApi = new TypedAPIClient({ baseUrl: 'https://api.example.com' });
const user = await typedApi.getUser(123); // Returns User directly| Method | Description |
|---|---|
get<T>(url, config?) |
GET request |
post<T>(url, data?, config?) |
POST request |
put<T>(url, data?, config?) |
PUT request |
delete<T>(url, config?) |
DELETE request |
patch<T>(url, data?, config?) |
PATCH request |
interceptors.addRequestInterceptor(fn) |
Add request middleware |
interceptors.addResponseInterceptor(fn) |
Add response middleware |
destroy() |
Clean up client resources |
All APIClient methods plus:
| Method | Description |
|---|---|
cache.getStats() |
Get cache statistics for dynamic routes |
cache.clearProxyCache() |
Clear dynamic route cache |
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
interface APIConfig {
baseUrl: string;
timeout?: number;
headers?: Record<string, string>;
withCredentials?: boolean;
retries?: number;
retryDelay?: number;
useKebabCase?: boolean;
// Method resolution options
defaultMethod?: HTTPMethod; // Fallback method, default: 'POST'
methodRules?: Record<string, HTTPMethod>; // Action-based wildcard rules
}interface APIResponse<T> {
success: boolean;
data?: T;
error?: {
code?: number;
message?: string;
};
}
interface ClientError {
message: string;
type: "network" | "timeout" | "abort" | "parse";
originalError?: Error;
response?: RawResponse;
}The project includes comprehensive test coverage using Jest and TypeScript.
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverageOur test suite covers all major functionality:
test/
βββ api-client.test.ts # Core APIClient functionality
βββ dynamic-client.test.ts # Dynamic routing and parameterized endpoints
βββ converter.test.ts # Case conversion utilities
βββ serializer.test.ts # Data serialization and FormData handling
βββ url-builder.test.ts # URL construction utilities
βββ setup.ts # Test configuration and global mocks
βββ globals.d.ts # TypeScript definitions for test environment
- β Basic HTTP methods (GET, POST, PUT, DELETE, PATCH)
- β Request/Response interceptors
- β Error handling and retry logic
- β FormData handling for file uploads
- β Configuration merging and validation
- β
Dynamic route generation (
api.users.getProfile()) - β
Parameterized endpoints (
api.users(123).follow()) - β
Multi-level routing (
api.admin.users.ban()) - β Case conversion (camelCase β kebab-case)
- β Nested parameters and complex data structures
- β FormData handling for file uploads
- β JSON serialization and content-type detection
- β Deep object transformation
- β File and Blob object detection
- β Edge cases and performance validation
- β URL construction with path segments
- β Query parameter handling
- β Base URL normalization
- β Special characters and encoding
- β camelCase to kebab-case conversion
- β kebab-case to camelCase conversion
- β Deep object key transformation
- β Array handling with nested objects
When contributing, follow these testing patterns:
import { APIClient } from '../src/core/api-client';
describe('APIClient', () => {
let client: APIClient;
beforeEach(() => {
client = new APIClient({ baseUrl: 'https://test.api' });
jest.clearAllMocks();
});
afterEach(() => {
client.destroy(); // Important: Clean up resources
});
it('should make GET request', async () => {
const mockResponse = { success: true, data: { id: 1 } };
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});
const result = await client.get('/test');
expect(result).toEqual(mockResponse);
});
});The project follows a modular architecture for maintainability and testability:
src/
βββ core/ # Main client classes
β βββ api-client.ts # Core HTTP client
β βββ dynamic-client.ts # Dynamic routing client
βββ libs/ # Reusable libraries
β βββ builders/ # Request and route builders
β βββ constants/ # Configuration constants
β βββ managers/ # Cache, interceptor, retry managers
β βββ parsers/ # Response parsing logic
β βββ security/ # Input sanitization
βββ utils/ # Utility functions
β βββ case-converter.ts # Case conversion logic
β βββ data-serializer.ts# Data transformation and FormData handling
β βββ url-builder.ts # URL construction
β βββ route-validator.ts# Route validation
βββ types/ # TypeScript definitions
β βββ config.ts # Configuration types
β βββ request.ts # Request types
β βββ response.ts # Response types
βββ interceptors/ # Pre-built interceptors
βββ logging.ts # Request/response logging
βββ cache.ts # Response caching
βββ timing.ts # Performance monitoring
- Separation of Concerns - Each component has a single responsibility
- Dependency Injection - Components accept dependencies, making testing easier
- Resource Management - Proper cleanup with
destroy()methods - Type Safety - Strict TypeScript configuration with full type coverage
- Immutability - Configurations are cloned, not mutated
- Error Boundaries - Graceful error handling at every level
// lib/api.ts
import { DynamicClient, IDynamicClient } from '@metis-w/api-client';
export const api: IDynamicClient = new DynamicClient({
baseUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Add auth interceptor
api.interceptors.addRequestInterceptor(async (config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers = { ...config.headers, Authorization: `Bearer ${token}` };
}
return config;
});
// pages/users/[id].tsx
import { api } from '../../lib/api';
export default function UserProfile({ user }) {
const handleFollow = async () => {
try {
await api.users(user.id).follow();
// Update UI
} catch (error) {
console.error('Failed to follow user:', error);
}
};
return <div>{/* Component JSX */}</div>;
}
export async function getServerSideProps(context) {
const { id } = context.params;
const user = await api.users(id).get();
return { props: { user: user.data } };
}import { useQuery, useMutation } from '@tanstack/react-query';
import { api } from '../lib/api';
// Custom hooks
export const useUser = (id: number) => {
return useQuery({
queryKey: ['user', id],
queryFn: () => api.users(id).get(),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
export const useFollowUser = () => {
return useMutation({
mutationFn: (userId: number) => api.users(userId).follow(),
onSuccess: () => {
// Invalidate and refetch user data
queryClient.invalidateQueries({ queryKey: ['user'] });
},
});
};| Feature | @metis-w/api-client | Axios |
|---|---|---|
| Bundle Size | ~15KB | ~45KB |
| Dynamic Routes | β
api.users.get() |
β Manual URLs |
| TypeScript | β Built-in | |
| Modern Fetch | β Native | β XMLHttpRequest |
| Tree Shaking | β Full support | |
| Interceptors | β Async/await | β Promise-based |
| Feature | @metis-w/api-client | Fetch API |
|---|---|---|
| Dynamic Routes | β
api.users(123).follow() |
β Manual |
| Error Handling | β Automatic | β Manual |
| Retries | β Built-in | β Manual |
| Interceptors | β Built-in | β Manual |
| File Uploads | β Automatic | β Manual FormData |
| TypeScript | β Full support |
// Import only what you need
import { APIClient } from '@metis-w/api-client';
import { requestLoggingInterceptor } from '@metis-w/api-client';
// Or import specific modules
import { DynamicClient } from '@metis-w/api-client/core';
import { CacheInterceptor } from '@metis-w/api-client/interceptors';
// Tree-shaking will remove unused code// Always destroy clients when done
const api = new APIClient(config);
// In cleanup (useEffect, componentWillUnmount, etc.)
useEffect(() => {
return () => api.destroy();
}, []);
// For DynamicClient, cache is automatically cleared
const dynamicApi = new DynamicClient(config);
useEffect(() => {
return () => dynamicApi.destroy(); // Clears both client and cache
}, []);import { CacheInterceptor } from '@metis-w/api-client';
const cache = new CacheInterceptor({
ttl: 5 * 60 * 1000, // 5 minutes
maxSize: 100 // Max 100 cached responses
});
// Add to client
api.interceptors.addRequestInterceptor(cache.requestInterceptor);
api.interceptors.addResponseInterceptor(cache.responseInterceptor);
// Check cache statistics
console.log(cache.getStats());Check out the comprehensive test suite for real-world examples:
test/api-client.test.ts- Core APIClient functionalitytest/dynamic-client.test.ts- Dynamic routing examplestest/converter.test.ts- Case conversion examplestest/serializer.test.ts- Data transformation examplestest/url-builder.test.ts- URL construction examples
We welcome contributions! Here's how to get started:
# Clone the repository
git clone https://github.com/metis-w/api-client.git
cd api-client
# Install dependencies
npm install
# Run tests
npm test
# Run tests in watch mode during development
npm run test:watch- Code Style - Follow the existing TypeScript patterns
- Tests Required - All new features must include tests
- Documentation - Update README.md for new features
- Type Safety - Maintain strict TypeScript compliance
- Performance - Consider bundle size and runtime performance
- Create feature branch:
git checkout -b feature/new-feature - Add implementation in appropriate
src/directory - Write comprehensive tests in
test/directory - Update documentation and examples
- Submit pull request with detailed description
When reporting bugs, please include:
- Environment details (Node.js version, TypeScript version)
- Minimal reproduction case
- Expected vs actual behavior
- Test case if possible
MIT Β© whiteakyloff
