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

@v1tor2003/command-api

v0.1.1

Published

A lightweight, zero-dependency, transport-agnostic TypeScript client library that implements the Command Pattern for HTTP/REST APIs.

Readme

@v1tor2003/command-api

A lightweight, zero-dependency, transport-agnostic TypeScript client library implementing the Command Pattern (In → Out) for HTTP/REST APIs.


Highlights

  • Command Pattern (In → Out): Encapsulate input parameters, URL serialization, and output typings into isolated, testable command units.
  • Go/Rust-Style Results (Result<T, E>): End unhandled promise rejections. Dispatch returns { data, error } discriminated unions that force clean, branch-safe error handling.
  • Zero-Dependency & Transport Agnostic: Ships with a high-performance, native Web Fetch transport. Easily plug in Axios, Ky, Got, or in-memory mocks without rewriting business logic.
  • Onion Middleware Pipeline: Bi-directional request/response interceptors supporting authentication injection, automatic retries with backoff, and tracing headers.
  • Built-in Observability: Microsecond-precision timing metrics, runtime log level toggles, and extensible log formatters (ready for Pino, Winston, or Datadog).
  • Modern Standards: Strict dual ESM/CJS distribution, full TypeScript declarations, and 100% verified TSDoc with API Extractor.

Table of Contents


Installation

# npm
npm install @v1tor2003/command-api

# pnpm
pnpm add @v1tor2003/command-api

# yarn
yarn add @v1tor2003/command-api

# bun
bun add @v1tor2003/command-api

Quick Start

1. Define a Command

Commands encapsulate input parameters, translate them into HTTP semantics, and strictly bind the return type:

import { BaseRequest, type HttpRequestContext } from '@v1tor2003/command-api';

interface GetUserInput {
  userId: string;
}

interface UserResponse {
  id: string;
  name: string;
  email: string;
}

export class GetUserCommand extends BaseRequest<GetUserInput, UserResponse> {
  toHttp(): HttpRequestContext {
    return {
      method: 'GET',
      path: `/users/${this.input.userId}`,
    };
  }
}

2. Initialize the Client and Dispatch

import { ApiClient, FetchTransport } from '@v1tor2003/command-api';
import { GetUserCommand } from './commands/get-user.command';

// Initialize with a base URL and transport
const api = new ApiClient({
  transport: new FetchTransport('https://api.example.com'),
  logging: true,
});

// Dispatch the command
const { data, error } = await api.send(new GetUserCommand({ userId: 'usr_123' }));

if (error) {
  console.error('Operation failed:', error.message);
  return;
}

// data is strictly inferred as UserResponse
console.log('User found:', data.name, data.email);

Core Concepts

1. Creating Commands (BaseRequest)

Every API endpoint is modeled as a discrete command inheriting from BaseRequest<TInput, TOutput>.

import { BaseRequest, type HttpRequestContext } from '@v1tor2003/command-api';

export interface CreateUserInput {
  name: string;
  email: string;
  role?: 'admin' | 'user';
}

export interface UserDTO {
  id: string;
  name: string;
  email: string;
  createdAt: string;
}

export class CreateUserCommand extends BaseRequest<CreateUserInput, UserDTO> {
  toHttp(): HttpRequestContext {
    return {
      method: 'POST',
      path: '/users',
      body: this.input,
      headers: {
        'Content-Type': 'application/json',
      },
    };
  }

  /**
   * Optional lifecycle hook to parse, validate, or transform raw response payloads.
   * Ideal for schema validation libraries (Zod, ArkType, Valibot).
   */
  transformResponse(raw: unknown): UserDTO {
    const data = raw as Record<string, unknown>;
    return {
      id: String(data.id),
      name: String(data.name).trim(),
      email: String(data.email).toLowerCase(),
      createdAt: String(data.createdAt),
    };
  }
}

2. Dispatching with ApiClient

ApiClient orchestrates the request lifecycle, executing middlewares, delegating to the transport layer, and normalizing exceptions into structured results.

import { ApiClient, FetchTransport } from '@v1tor2003/command-api';

const api = new ApiClient({
  transport: new FetchTransport({
    baseUrl: 'https://api.example.com/v1',
    headers: {
      'User-Agent': 'MyApp/1.0.0',
    },
  }),
  logging: true,
});

3. Error Handling (Result<T, E>)

ApiClient.send() never throws unhandled promise rejections for HTTP failures or network crashes. Instead, it returns a Result<T, E> discriminated union:

const result = await api.send(new CreateUserCommand(payload));

// Discriminated union check
if (result.error) {
  // result.error is typed as Error (or HttpError)
  console.error(`Error: ${result.error.message}`);
  return;
}

// TypeScript automatically narrows result.data to UserDTO
console.log(`Created user with ID: ${result.data.id}`);

You can also use the built-in functional guards:

import { isOk, isErr } from '@v1tor2003/command-api';

if (isErr(result)) {
  console.error(result.error.message);
} else if (isOk(result)) {
  console.log(result.data.name);
}

4. Middleware Pipeline

Middlewares run in an onion-style execution model (similar to Koa or Express). Middlewares can inspect and mutate the request context before execution, and capture or modify responses upon return.

Authentication Middleware

import type { Middleware } from '@v1tor2003/command-api';

export const authMiddleware: Middleware = async (ctx, next, command) => {
  const token = await getAuthToken();
  ctx.headers = {
    ...ctx.headers,
    Authorization: `Bearer ${token}`,
  };

  return next();
};

Automatic Retry Middleware

Because next() can be invoked downstream multiple times, implementing retry loops with exponential backoff is straightforward:

import type { Middleware } from '@v1tor2003/command-api';

export const retryMiddleware = (maxRetries = 2): Middleware => {
  return async (ctx, next) => {
    let attempts = 0;
    while (attempts < maxRetries) {
      try {
        return await next();
      } catch (err) {
        attempts += 1;
        if (attempts >= maxRetries) throw err;
        await new Promise((res) => setTimeout(res, 1000 * attempts));
      }
    }
  };
};

// Register via fluent chaining
api.use(authMiddleware).use(retryMiddleware(3));

5. Pluggable Transports

Business logic depends solely on the ITransport abstraction, not on a concrete networking library:

export interface ITransport {
  send<T = unknown>(ctx: HttpRequestContext): Promise<T>;
}

Built-in FetchTransport

FetchTransport uses native fetch with automatic JSON serialization, binary body detection (FormData, Blob, ArrayBuffer), empty status code handling (204 No Content, 205 Reset Content), and query string building.

const transport = new FetchTransport({
  baseUrl: 'https://api.example.com',
  fetch: customFetchImplementation, // Optional custom fetch override
});

Custom Adapter (e.g., Axios)

To use another HTTP engine, simply implement ITransport:

import axios from 'axios';
import type { ITransport, HttpRequestContext } from '@v1tor2003/command-api';

export class AxiosTransport implements ITransport {
  constructor(private readonly baseUrl: string) {}

  async send<T = unknown>(ctx: HttpRequestContext): Promise<T> {
    const response = await axios.request<T>({
      baseURL: this.baseUrl,
      url: ctx.path,
      method: ctx.method,
      params: ctx.query,
      headers: ctx.headers,
      data: ctx.body,
      signal: ctx.signal,
    });

    return response.data;
  }
}

6. Observability & Logging

command-api includes structured performance timing and telemetry hooks:

const api = new ApiClient({
  transport: new FetchTransport('https://api.example.com'),
  logging: {
    enabled: true,
    // Custom log formatting (e.g. forward to Datadog or Pino)
    formatter: (entry) => {
      logger.info({
        command: entry.command,
        method: entry.method,
        path: entry.path,
        durationMs: entry.durationMs,
        status: entry.error ? 'FAILED' : 'SUCCESS',
      });
      return undefined; // Handled externally
    },
  },
});

// Dynamically adjust logging at runtime
api.setLogging(false);

Default log format:

[GetUserCommand] GET /users/123 - 18.40ms (SUCCESS)
[CreateUserCommand] POST /users - 34.12ms (ERROR: Bad Request)

Testing & Mocking

Because commands are pure declarative classes, unit testing is fast and avoids fragile HTTP server mocking:

import { describe, expect, it } from 'vitest';
import { GetUserCommand } from './get-user.command';

describe('GetUserCommand', () => {
  it('translates input parameters to HTTP context', () => {
    const command = new GetUserCommand({ userId: '42' });

    expect(command.commandName).toBe('GetUserCommand');
    expect(command.toHttp()).toEqual({
      method: 'GET',
      path: '/users/42',
    });
  });

  it('transforms raw response', () => {
    const command = new GetUserCommand({ userId: '42' });
    const transformed = command.transformResponse?.({
      id: 42,
      name: '  Alice  ',
      email: '[email protected]',
    });

    expect(transformed).toEqual({
      id: '42',
      name: 'Alice',
      email: '[email protected]',
    });
  });
});

API Reference

| Export | Type | Description | | :------------------------------- | :------------- | :-------------------------------------------------------------------------------- | | ApiClient | Class | Core client dispatcher executing commands via middlewares and transport. | | BaseRequest<TIn, TOut> | Abstract Class | Base command class binding input parameters, HTTP translation, and output types. | | FetchTransport | Class | Default zero-dependency transport using the Web Fetch API. | | HttpUrlBuilder | Class | Encapsulates path resolution and query parameter serialization. | | HttpPayloadResolver | Class | Encapsulates request header merging and body evaluation. | | HttpResponseHandler | Class | Evaluates response status, 204/205 empty states, and JSON decoding. | | MiddlewarePipeline | Class | Encapsulates onion-style middleware registration and execution. | | HttpError | Class | Standardized HTTP error containing status, statusText, data, and headers. | | LoggerService | Class | Handles execution timing, formatting, and console telemetry. | | ok(data) | Function | Factory creating a successful Result ({ data, error: null }). | | err(error) | Function | Factory creating an error Result ({ data: null, error }). | | isOk(result) / isErr(result) | Functions | TypeScript type-guard assertions for Result unions. |


License

MIT © Vítor Pires