@metis-w/api-client
v1.2.6
Published
Modern TypeScript HTTP API client with dynamic routes, parameterized endpoints, interceptors, and advanced features
Downloads
40
Maintainers
Readme
Metis API Client
Features
- 🎯 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
Installation
npm install @metis-w/api-clientQuick Start
Basic Usage
import { 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: '[email protected]' });
// 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' });Subpath imports (опційно)
import { DynamicClient } from '@metis-w/api-client/core';
import { CacheInterceptor } from '@metis-w/api-client/interceptors';Dynamic Routes
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' });Parameterized Routes
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=jsonDirect Call Logic
- 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
HTTP Method Resolution
The DynamicClient intelligently determines the appropriate HTTP method based on several factors:
1. Explicit Method (Highest Priority)
// 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 method2. Direct HTTP Methods
// 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/patch3. Semantic Analysis (Auto-detection)
The 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(); // PATCH4. Custom Method Rules
Define 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)5. Default Method
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)Priority Order
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)
File Uploads
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']
});TypeScript Support
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: '[email protected]',
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: "[email protected]" }); // 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/followGeneric Type Support
// 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 directlyAPI Reference
APIClient
| 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 |
DynamicClient
All APIClient methods plus:
| Method | Description |
|--------|-------------|
| cache.getStats() | Get cache statistics for dynamic routes |
| cache.clearProxyCache() | Clear dynamic route cache |
Configuration
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
}Response Format
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;
}Testing
The project includes comprehensive test coverage using Jest and TypeScript.
Running Tests
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverageTest Structure
Our 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 environmentKey Test Scenarios
APIClient Tests
- ✅ 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
DynamicClient Tests
- ✅ 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
Data Serialization Tests
- ✅ 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 Builder Tests
- ✅ URL construction with path segments
- ✅ Query parameter handling
- ✅ Base URL normalization
- ✅ Special characters and encoding
Case Conversion Tests
- ✅ camelCase to kebab-case conversion
- ✅ kebab-case to camelCase conversion
- ✅ Deep object key transformation
- ✅ Array handling with nested objects
Writing Tests
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);
});
});Architecture
The project follows a modular architecture for maintainability and testability:
Core Components
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 monitoringDesign Principles
- 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
Integration Examples
Next.js Integration
// 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 } };
}React Query Integration
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'] });
},
});
};Comparison with Other Libraries
vs. Axios
| Feature | @metis-w/api-client | Axios |
|---------|-------------------|-------|
| Bundle Size | ~15KB | ~45KB |
| Dynamic Routes | ✅ api.users.get() | ❌ Manual URLs |
| TypeScript | ✅ Built-in | ⚠️ Community types |
| Modern Fetch | ✅ Native | ❌ XMLHttpRequest |
| Tree Shaking | ✅ Full support | ⚠️ Limited |
| Interceptors | ✅ Async/await | ✅ Promise-based |
vs. Native Fetch
| 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 | ⚠️ Basic |
Performance Considerations
Bundle Size Optimization
// 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 codeMemory Management
// 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
}, []);Caching Strategy
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());Examples
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
🤝 Contributing
We welcome contributions! Here's how to get started:
Development Setup
# 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:watchContribution Guidelines
- 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
Adding New Features
- 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
Reporting Issues
When reporting bugs, please include:
- Environment details (Node.js version, TypeScript version)
- Minimal reproduction case
- Expected vs actual behavior
- Test case if possible
📄 License
MIT © whiteakyloff

