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

@nghien-ot/rux

v0.2.5

Published

A declarative, type-safe HTTP client for TypeScript with zero runtime dependencies.

Readme

Rux

npm package GitHub repository Buy Me a Coffee

A type-safe HTTP client for TypeScript. Rux validates through the Standard Schema v1 protocol and has no runtime dependencies.

See CONTRIBUTION.md for development and contribution rules. See SECURITY.md for vulnerability reporting and security responsibilities.

Install

bun add @nghien-ot/rux
# or: npm install @nghien-ot/rux

createClient

Define endpoints with a method, a path, and optional Standard Schema validators. Calls always resolve to RuxResult; inspect ok before reading value.

import { createClient } from "@nghien-ot/rux";
import { z } from "zod";

const user = z.object({
  id: z.string(),
  name: z.string(),
  createdAt: z.string().transform((value) => new Date(value)),
});

const api = createClient({
  baseUrl: "https://api.example.com",
  request: { headers: { "x-client": "web" } },
  timeoutMs: 5_000,
  endpoints: {
    getUser: {
      method: "GET",
      path: "/users/:id[string]",
      response: user,
    },
  },
});

const result = await api.getUser({ params: { id: "42" } });
if (result.ok) {
  result.value.createdAt; // Date: parsed Zod transform output
} else {
  console.error(result.error.type, result.error.message);
}

Zod implements Standard Schema v1, so pass a Zod schema directly. Rux does not import or bundle Zod; install Zod only when your application uses it. Any structurally compatible Standard Schema v1 implementation works.

InferInput<S> is the input accepted by a schema, InferOutput<S> is its parsed output, and Infer<S> aliases InferOutput<S>.

Usage guide

Define schemas

Rux accepts any validator implementing Standard Schema v1. Zod is optional application code, not a Rux dependency:

npm install @nghien-ot/rux zod
import { z } from "zod";

const userInput = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

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

const apiError = z.object({
  code: z.string(),
  detail: z.string().optional(),
});

Zod transforms are applied before Rux returns a value or serializes a request. InferInput<typeof userInput> and InferOutput<typeof userInput> expose the input/output types when needed.

Configure endpoints

createClient requires one absolute baseUrl and an endpoints map. Endpoint names become client methods.

const api = createClient({
  baseUrl: "https://api.example.com/v1",
  endpoints: {
    getUser: {
      method: "GET",
      path: "/users/:id[string]",
      response: userResponse,
      error: apiError,
    },
    createUser: {
      method: "POST",
      path: "/users",
      body: userInput,
      response: userResponse,
      error: apiError,
    },
  },
});

Endpoint fields:

| Field | Purpose | | ----------- | ------------------------------------------------------------------------------------------------------------- | | method | GET, POST, PUT, PATCH, or DELETE. Method belongs to the endpoint. | | path | Relative path beginning with /. Typed parameters use :name[string], :name[number], or :name[boolean]. | | request | Endpoint-level RequestInit options, excluding method and body. | | timeoutMs | Endpoint timeout override. | | query | Standard Schema for query input. | | body | Standard Schema for POST, PUT, or PATCH body input. | | response | Standard Schema for successful JSON output. | | error | Standard Schema for non-2xx JSON error output. |

Call endpoints

Path parameters, query input, body input, request overrides, and timeout are supplied at invocation:

const result = await api.createUser({
  body: { name: "Ada", email: "[email protected]" },
  request: { headers: { "x-request-id": "request-123" } },
  timeoutMs: 3_000,
});

if (result.ok) {
  console.log(result.value.id);
}

Typed path parameters are encoded with encodeURIComponent:

await api.getUser({ params: { id: "user/42" } });
// Requests /users/user%2F42

Query values support strings, numbers, booleans, null, undefined, and arrays. undefined is omitted, null becomes an empty query value, and arrays produce repeated query parameters. A query schema receives input before serialization, so schema transforms can normalize it.

Body schemas receive call input before serialization. Rux serializes their parsed output as JSON and adds content-type: application/json when no content type was supplied. GET and DELETE endpoints do not expose a typed body option.

Layer request options

Transport options resolve from client to endpoint to invocation. Later values override earlier scalar values:

const api = createClient({
  baseUrl: "https://api.example.com",
  request: {
    credentials: "include",
    headers: { "x-source": "client", "x-shared": "client" },
  },
  timeoutMs: 10_000,
  endpoints: {
    updateUser: {
      method: "PATCH",
      path: "/users/:id[string]",
      request: {
        cache: "no-store",
        headers: { "X-Source": "endpoint", "x-endpoint": "true" },
      },
      body: userInput,
      response: userResponse,
    },
  },
});

await api.updateUser({
  params: { id: "42" },
  body: { name: "Ada", email: "[email protected]" },
  request: {
    headers: { "X-SOURCE": "call", "x-shared": "call" },
  },
  timeoutMs: 2_000,
});

The final request has x-source: call, x-shared: call, x-endpoint: true, credentials: "include", and cache: "no-store". Header names merge case-insensitively. baseUrl and injected fetch are client-only; endpoint method, path parameters, query, and body are endpoint/call concerns.

Handle results and typed errors

Calls never throw for expected request failures. Every call resolves to RuxResult<Success, Failure>:

const result = await api.getUser({ params: { id: "42" } });

if (result.ok) {
  // result.value: InferOutput<typeof userResponse>
  console.log(result.value.name);
} else {
  switch (result.error.type) {
    case "http":
      console.error(result.error.status, result.error.data);
      break;
    case "validation":
      console.error(result.error.phase, result.error.issues);
      break;
    case "network":
      console.error(result.error.message, result.error.cause);
      break;
    case "request":
      console.error(result.error.message, result.error.cause);
      break;
  }
}

Error variants:

  • request: invalid URL, invalid path parameters, body serialization, timeout, or caller abort.
  • network: fetch failed before receiving an HTTP response.
  • http: non-2xx response. status is always present. Without error, Rux parses JSON into data: unknown, falling back to raw response text when it is not JSON. With error, data is the validated parsed output of the configured schema.
  • validation: body, query, response, or error payload failed validation. phase identifies the boundary and issues contains Standard Schema issues.

Response JSON is validated after parsing, so response transforms return parsed output. Empty successful responses return undefined when no response schema is configured. A non-empty successful response requires response; otherwise Rux returns a validation error. Invalid JSON returns a validation error.

Timeout, abort, and testing

Use timeoutMs for automatic cancellation. Pass an AbortSignal through any request layer for caller-controlled cancellation; Rux combines it with its timeout signal:

const controller = new AbortController();

const pending = api.getUser({
  params: { id: "42" },
  request: { signal: controller.signal },
});

controller.abort();
const result = await pending;
// result.ok === false && result.error.type === "request"

Inject fetch through client configuration for deterministic tests or custom runtimes:

const api = createClient({
  baseUrl: "https://api.example.com",
  fetch: async (input, init) => {
    // test double, fetch wrapper, or runtime-specific implementation
    return fetch(input, init);
  },
  endpoints: {
    health: { method: "GET", path: "/health" },
  },
});

Errors

Every endpoint returns Promise<RuxResult<Success, Failure>>. Failure values have a typed RuxError with one of these variants:

| Type | Meaning | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | request | Invalid URL, path input, serialization, timeout, or caller abort | | network | Fetch failed before an HTTP response | | http | Non-2xx response. Without error, data is parsed JSON when possible or raw text when not JSON. With error, data is the typed schema output. | | validation | Body, query, response, or typed error payload failed validation |

License

MIT