@metis-w/api-client
Version:
Modern TypeScript HTTP API client with dynamic routes, parameterized endpoints, interceptors, and advanced features
748 lines (582 loc) β’ 24.2 kB
Markdown
# Metis API Client
[](https://npmjs.com/package/@metis-w/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
```bash
npm install @metis-w/api-client
```
## Quick Start
### Basic Usage
```typescript
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: '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' });
```
#### Subpath imports (ΠΎΠΏΡΡΠΉΠ½ΠΎ)
```typescript
import { DynamicClient } from '@metis-w/api-client/core';
import { CacheInterceptor } from '@metis-w/api-client/interceptors';
```
### Dynamic Routes
```typescript
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:
```typescript
// 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
```
#### Direct 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)
```typescript
// 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
```
#### 2. Direct HTTP Methods
```typescript
// 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/patch
```
#### 3. Semantic Analysis (Auto-detection)
The library automatically detects the intent from action names:
```typescript
// 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(); // PATCH
```
#### 4. 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:
```typescript
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:
```typescript
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:
1. **Explicit method** in payload (`{ method: "DELETE" }`)
2. **Direct HTTP method** (`get`, `post`, `put`, `delete`, `patch`)
3. **Custom method rules** (from `methodRules` config)
4. **Semantic analysis** (action name patterns)
5. **Default method** (from `defaultMethod` config, defaults to `POST`)
### File Uploads
```typescript
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.
```typescript
// 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
```
### Generic Type Support
```typescript
// 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
```
## API 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
```typescript
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
```typescript
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
```bash
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
```
### Test 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 environment
```
### Key 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:
```typescript
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 monitoring
```
### Design Principles
1. **Separation of Concerns** - Each component has a single responsibility
2. **Dependency Injection** - Components accept dependencies, making testing easier
3. **Resource Management** - Proper cleanup with `destroy()` methods
4. **Type Safety** - Strict TypeScript configuration with full type coverage
5. **Immutability** - Configurations are cloned, not mutated
6. **Error Boundaries** - Graceful error handling at every level
## Integration Examples
### Next.js Integration
```typescript
// 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
```typescript
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
```typescript
// 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
```
### Memory Management
```typescript
// 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
```typescript
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`](./test/api-client.test.ts) - Core APIClient functionality
- [`test/dynamic-client.test.ts`](./test/dynamic-client.test.ts) - Dynamic routing examples
- [`test/converter.test.ts`](./test/converter.test.ts) - Case conversion examples
- [`test/serializer.test.ts`](./test/serializer.test.ts) - Data transformation examples
- [`test/url-builder.test.ts`](./test/url-builder.test.ts) - URL construction examples
## π€ Contributing
We welcome contributions! Here's how to get started:
### Development Setup
```bash
# 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
```
### Contribution Guidelines
1. **Code Style** - Follow the existing TypeScript patterns
2. **Tests Required** - All new features must include tests
3. **Documentation** - Update README.md for new features
4. **Type Safety** - Maintain strict TypeScript compliance
5. **Performance** - Consider bundle size and runtime performance
### Adding New Features
1. Create feature branch: `git checkout -b feature/new-feature`
2. Add implementation in appropriate `src/` directory
3. Write comprehensive tests in `test/` directory
4. Update documentation and examples
5. 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](https://github.com/metis-w)
---