npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@metis-w/api-client

v1.2.6

Published

Modern TypeScript HTTP API client with dynamic routes, parameterized endpoints, interceptors, and advanced features

Downloads

40

Readme

Metis API Client

NPM

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-client

Quick 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=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)

// 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

// 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:

// 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:

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:

  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

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/follow

Generic 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 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

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: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:

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

// 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 code

Memory 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:

🤝 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: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