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 🙏

© 2025 – Pkg Stats / Ryan Hefner

zlient

v1.0.11

Published

A type-safe HTTP client framework with Zod validation for building robust API clients

Readme

zlient

A type-safe HTTP client framework with Zod validation for building robust API clients.

Features

  • 🔒 Type-safe: Full TypeScript support with automatic type inference
  • Runtime validation: Zod schemas for request/response validation
  • 🔄 Retry logic: Built-in configurable retry strategies with exponential backoff
  • 🎯 Authentication: Multiple auth providers (API Key, Bearer Token, Custom)
  • 🪝 Interceptors: Before request and after response hooks
  • ⏱️ Timeouts: Configurable request timeouts
  • 📦 Multiple endpoints: Easy service separation with base URL mapping
  • 📊 Observability: Built-in logging and metrics collection
  • 🎨 Developer Experience: Comprehensive JSDoc, helper methods, great error messages
  • 🏢 Enterprise-ready: Production-grade logging, metrics, and monitoring support

Installation

npm install zlient zod
# or
yarn add zlient zod
# or
pnpm add zlient zod
# or
bun add zlient zod

Quick Start

import { AuthProvider, BaseEndpoint, ClientOptions, HttpClient, HTTPMethod, RequestOptions } from "zlient";
import z from "zod";

/**
 * Schemas
 */
const todoItem = z.object({
    userId: z.number(),
    id: z.number(),
    title: z.string(),
    completed: z.boolean(),
});

const ListTodosRequest = z.object({});
const ListTodosResponse = z.array(todoItem);

const GetTodoRequest = z.object({
    id: z.number(),
});
const GetTodoResponse = todoItem;


/**
 * Endpoints
 */
class ListTodos extends BaseEndpoint<typeof ListTodosRequest, typeof ListTodosResponse> {
    protected readonly method = HTTPMethod.GET;
    protected readonly path = "/todos";
    constructor(client: HttpClient) { super(client, { requestSchema: ListTodosRequest, responseSchema: ListTodosResponse }); }
}

class GetTodo extends BaseEndpoint<typeof GetTodoRequest, typeof GetTodoResponse> {
    protected readonly method = HTTPMethod.GET;
    protected readonly path = (args: z.infer<typeof GetTodoRequest>) => `/todos/${args.id}`;
    constructor(client: HttpClient) { super(client, { requestSchema: GetTodoRequest, responseSchema: GetTodoResponse }); }
}

/**
 * Service
 */
class TodosService {
    constructor(private client: HttpClient) { }
    list(args: z.infer<typeof ListTodosRequest>, options?: RequestOptions) { return new ListTodos(this.client).call(args, options); }
    get(args: z.infer<typeof GetTodoRequest>, options?: RequestOptions) { return new GetTodo(this.client).call(args, options); }
}

/**
 * SDK class for initialization
 */
export class SDK {
    readonly http: HttpClient;
    readonly todos: TodosService;

    constructor(opts: ClientOptions & { auth?: AuthProvider }) {
        this.http = new HttpClient(opts);
        if (opts.auth) this.http.setAuth(opts.auth);

        // Initialize services
        this.todos = new TodosService(this.http);
    }
}


/**
 * Usage example
 */
const sdk = new SDK({
    baseUrls: {
        default: "https://jsonplaceholder.typicode.com",
    },
    headers: {
        "X-SDK-Name": "example-sdk",
        "X-SDK-Version": "1.0.0",
    },
    retry: { maxRetries: 2, baseDelayMs: 100, jitter: 0.2, retryMethods: ["GET"] },
    timeout: { requestTimeoutMs: 5000 },
});

async function demo() {
    const todos = await sdk.todos.list({});
    console.log(todos);

    const todo = await sdk.todos.get({ id: 1 });
    console.log(todo);
}

demo().catch(console.error);

Core Concepts

HttpClient

The main HTTP client that handles requests, retries, authentication, and interceptors.

import { HttpClient, NoAuth } from 'zlient';

const client = new HttpClient({
  baseUrls: {
    default: 'https://api.example.com',
    v2: 'https://api-v2.example.com',
  },
  headers: {
    'Content-Type': 'application/json',
  },
  retry: {
    maxRetries: 3,
    baseDelayMs: 1000,
    jitter: 0.2,
  },
  timeout: {
    requestTimeoutMs: 30000,
  },
  auth: new NoAuth(),
});

Authentication

Bearer Token

import { BearerTokenAuth } from 'zlient';

const auth = new BearerTokenAuth(async () => {
  // Fetch token from your auth service
  return await getAccessToken();
});

client.setAuth(auth);

API Key

import { ApiKeyAuth } from 'zlient';

// Header-based
const auth = new ApiKeyAuth({
  header: 'X-API-Key',
  value: 'your-api-key',
});

// Query parameter-based
const auth = new ApiKeyAuth({
  query: 'apiKey',
  value: 'your-api-key',
});

Custom Auth

import { AuthProvider } from 'zlient';

class CustomAuth implements AuthProvider {
  async apply({ init }) {
    init.headers = {
      ...init.headers,
      'X-Custom-Auth': 'custom-value',
    };
  }
}

BaseEndpoint

Create type-safe endpoints with automatic validation:

import { BaseEndpoint, HttpClient } from 'zlient';
import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

const UserResponseSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  createdAt: z.string().datetime(),
});

class CreateUserEndpoint extends BaseEndpoint<
  typeof CreateUserSchema,
  typeof UserResponseSchema
> {
  protected method = 'POST' as const;
  protected path = '/users';
  
  constructor(client: HttpClient) {
    super(client, {
      requestSchema: CreateUserSchema,
      responseSchema: UserResponseSchema,
    });
  }
}

// Usage
const endpoint = new CreateUserEndpoint(client);
const user = await endpoint.call({
  name: 'John Doe',
  email: '[email protected]',
});

Interceptors

Add hooks to inspect or modify requests and responses:

const client = new HttpClient({
  baseUrls: { default: 'https://api.example.com' },
  interceptors: {
    beforeRequest: [
      async ({ url, init }) => {
        console.log('Making request to:', url);
      },
    ],
    afterResponse: [
      async ({ request, response, parsed }) => {
        console.log('Response received:', response.status);
      },
    ],
  },
});

Common Schemas

The package includes common reusable schemas:

import { Id, Timestamps, Meta, ApiErrorSchema, Envelope } from 'zlient';

// Use in your schemas
const MyEntitySchema = z.object({
  id: Id,
  name: z.string(),
  ...Timestamps.shape,
});

// Wrap responses in an envelope
const MyResponseSchema = Envelope(MyEntitySchema);

Advanced Usage

Multiple Base URLs

const client = new HttpClient({
  baseUrls: {
    default: 'https://api.example.com',
    auth: 'https://auth.example.com',
    cdn: 'https://cdn.example.com',
  },
});

// Use specific base URL for a request
await endpoint.call(data, { baseUrlKey: 'auth' });

Request Options

await endpoint.call(data, {
  headers: { 'X-Custom-Header': 'value' },
  baseUrlKey: 'v2',
  signal: abortController.signal,
  query: { filter: 'active', page: 1 },
});

Logging and Metrics

Structured Logging

import { HttpClient, ConsoleLogger, LogLevel } from 'zlient';

const client = new HttpClient({
  baseUrls: { default: 'https://api.example.com' },
  logger: new ConsoleLogger(LogLevel.INFO),
});

// All requests are automatically logged with duration, status, etc.

Metrics Collection

import { HttpClient, InMemoryMetricsCollector } from 'zlient';

const metrics = new InMemoryMetricsCollector();
const client = new HttpClient({
  baseUrls: { default: 'https://api.example.com' },
  metrics,
});

// View metrics summary
const summary = metrics.getSummary();
console.log(`Success rate: ${(summary.successful / summary.total * 100).toFixed(2)}%`);
console.log(`Avg duration: ${summary.avgDurationMs}ms`);

Custom Logger/Metrics Integration

import { Logger, LogEntry, MetricsCollector, RequestMetrics } from 'zlient';

// Integrate with your logging service (e.g., DataDog, CloudWatch)
class CustomLogger implements Logger {
  log(entry: LogEntry) {
    // Send to your logging service
    myLoggingService.log(entry);
  }
}

class CustomMetrics implements MetricsCollector {
  collect(metrics: RequestMetrics) {
    // Send to your metrics service (e.g., Prometheus, DataDog)
    dogstatsd.histogram('http.request.duration', metrics.durationMs);
  }
}

const client = new HttpClient({
  baseUrls: { default: 'https://api.example.com' },
  logger: new CustomLogger(),
  metrics: new CustomMetrics(),
});

Convenience Methods

// Use shortcuts instead of full request() method
const { data: users } = await client.get('/users', { query: { page: 1 } });
const { data: user } = await client.post('/users', { name: 'John' });
const { data: updated } = await client.put('/users/1', { name: 'Jane' });
const { data: patched } = await client.patch('/users/1', { email: '[email protected]' });
await client.delete('/users/1');

Error Handling

import { ApiError } from 'zlient';

try {
  await endpoint.call(data);
} catch (error) {
  if (error instanceof ApiError) {
    console.error('API Error:', error.message);
    console.error('Status:', error.status);
    console.error('Details:', error.details);
    
    // Check error type
    if (error.isValidationError()) {
      console.error('Validation errors:', error.zodError?.issues);
    }
    if (error.isClientError()) {
      console.error('Client error (4xx)');
    }
    if (error.isServerError()) {
      console.error('Server error (5xx)');
    }
    
    // Get full error details
    console.error(JSON.stringify(error.toJSON(), null, 2));
  }
}

Building from Source

# Clone the repository
git clone <your-repo-url>
cd zlient

# Install dependencies
bun install

# Build the package
bun run build

License

MIT

Contributing

Contributions are welcome! Please open an issue or submit a pull request.