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.1.2-dev

Published

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

Readme

Rux

GitHub

[!WARNING] Development preview — This library is still in active development. Breaking changes are expected until a stable 1.x release. Pin dependency versions and check release notes before upgrading.

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

Define your API with defineClient and get typed endpoint methods, runtime validation for bodies and responses, and flexible error handling.

Features

  • Zero runtime dependencies — no third-party runtime packages; schemas are plain objects
  • Declarative schemas — plain object literals; use as const where you want literal inference
  • Compile-time inferenceSchemaToType / Infer derive TypeScript types from schemas
  • Path params — bracket DSL :name[string], :name[number], :name[boolean] for typed params and URL substitution
  • Runtime validation — request body (when defined), queryParams (when defined), and every response body
  • Error modes — per-client or per-call: result, throw, or fallback (with defaultValue)
  • Method-aware typesbody only on POST, PUT, PATCH
  • Auth — bearer, basic, and custom header configuration
  • Standalone validationvalidate, validateResponse, handleValidation for non-HTTP use

Installation

bun add @nghien-ot/rux

Or with npm/pnpm:

npm install @nghien-ot/rux

Quick start

import { defineClient } from "@nghien-ot/rux";

const userResponse = {
  type: "object",
  properties: {
    id: "string",
    name: "string",
    email: "string",
  },
} as const;

const api = defineClient({
  baseUrl: "https://api.example.com",
  errorMode: "result",
  endpoints: {
    getUser: {
      method: "GET",
      path: "/users/:id[string]",
      response: userResponse,
    },
    createUser: {
      method: "POST",
      path: "/users",
      response: userResponse,
      body: {
        type: "object",
        properties: { name: "string", email: "string" },
      },
    },
    deleteUser: {
      method: "DELETE",
      path: "/users/:id[string]",
      response: { type: "object", properties: { success: "boolean" } } as const,
    },
  },
});

Use :id[string] (or [number] / [boolean]) so params is typed and segments are substituted in the URL. A plain :id without brackets is not replaced at runtime.

Schema format

| Schema | TypeScript type | | ----------------------------------------- | ---------------------------------- | | "string" | string | | "number" | number | | "boolean" | boolean | | "unknown" | unknown | | { type: "object", properties: { ... } } | Object with required/optional keys | | { type: "array", items: ... } | T[] |

Optional object fields use { type: "string", optional: true }. Nullable fields use nullable: true.

import type { SchemaToType } from "@nghien-ot/rux";

type User = SchemaToType<typeof userResponse>;

Infer usage (schema, client methods, and endpoint definitions)

Infer maps schemas to TypeScript types and can pick a single slice from a client endpoint function or an endpoint definition.

  • Schema: Infer<S> is the same as SchemaToType<S>.
  • Client method: Infer<typeof api.getUser, K> where K is one of "response", "path", "params", "query", or "body" (only for POST/PUT/PATCH). Omit the second argument to get the response type: Infer<typeof api.getUser>.
  • Endpoint definition: same keys, e.g. Infer<(typeof endpoints)["getUser"], "response">.
  • Second argument: pass exactly one key per call (not a union of keys). TypeScript will only accept keys that make sense for the endpoint (e.g. GET/DELETE do not allow "body").
import type { Infer } from "@nghien-ot/rux";

const userResponse = {
  type: "object",
  properties: { id: "string", name: "string" },
} as const;

type User = Infer<typeof userResponse>; // { id: string; name: string }

const endpoints = {
  getUser: {
    method: "GET",
    path: "/users/:id[string]",
    response: userResponse,
  },
} as const;

type GetUserResponse = Infer<(typeof endpoints)["getUser"], "response">;
type GetUserPath = Infer<(typeof endpoints)["getUser"], "path">;
type GetUserParams = Infer<(typeof endpoints)["getUser"], "params">;

After defineClient, use typeof api.getUser the same way:

type Id = Infer<typeof api.getUser, "params">["id"];

Usage

Result mode (default)

Each call returns RuxResult<T>:

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

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

Throw mode

Unwraps success or throws a RuxError object (not instanceof Error):

const user = await api.getUser({
  params: { id: "1" },
  errorMode: "throw",
});

Fallback mode

On failure, returns defaultValue:

const user = await api.getUser({
  params: { id: "1" },
  errorMode: "fallback",
  defaultValue: { id: "", name: "Unknown", email: "" },
});

If the client uses errorMode: "fallback", every call must supply defaultValue (or the promise rejects with a clear error).

Client-level error mode

const api = defineClient({
  baseUrl: "https://api.example.com",
  errorMode: "throw",
  endpoints: {
    /* ... */
  },
});

const user = await api.getUser({ params: { id: "1" } });

Path parameters (bracket DSL)

Typed params are derived from :name[string], :name[number], or :name[boolean] in path. Example:

path: "/users/:id[string]/posts/:postId[number]";
// params: { id: string; postId: number }

Request body

Only on POST, PUT, PATCH. With a body schema, the payload is typed and validated before fetch.

Query string

  • query: Record<string, string> (and values that stringify for arrays — see below). Extra keys are allowed when queryParams is defined (they are still appended to the URL).
  • queryParams on the endpoint: describe typed query arguments (required, nullable, type: "array" with items). Invalid query objects fail validation before the request is sent.

Non-array object values in query are skipped when serializing (no nested JSON in the query string).

Auth configuration

const api = defineClient({
  baseUrl: "https://api.example.com",
  auth: { type: "bearer", token: "my-jwt-token" },
  endpoints: { /* ... */ },
});

// Basic auth
auth: {
  type: "basic",
  credentials: { username: "admin", password: "secret" },
},

// Custom header
auth: {
  type: "custom",
  header: { name: "x-api-key", value: "my-key" },
},

Default header merge order: content-type, auth, client headers, endpoint headers, per-call headers (later wins).

unwrapOrThrow / unwrapOrDefault

import { unwrapOrThrow, unwrapOrDefault } from "@nghien-ot/rux";

const result = await api.getUser({ params: { id: "1" } });
const user = unwrapOrThrow(result);
const safe = unwrapOrDefault(result, { id: "", name: "?", email: "" });

Standalone validation

import { validate, validateResponse, handleValidation } from "@nghien-ot/rux";

const schema = {
  type: "object",
  properties: { name: "string", age: "number" },
} as const;

validate(schema, { name: "John", age: 30 }); // true

const checked = validateResponse<typeof schema>(schema, data);

// Modes for handleValidation use the string "default" (not "fallback"):
const asResult = handleValidation(schema, data, "result");
const value = handleValidation(schema, data, "throw");
const fallback = handleValidation(schema, data, "default", {
  name: "",
  age: 0,
});

Naming note: the HTTP client uses errorMode: "fallback" and defaultValue. handleValidation uses mode "default" and a fourth argument named fallback.

Error types

interface RuxError {
  type: "network" | "validation" | "http";
  status?: number;
  message: string;
  cause?: unknown;
}

| type | When | | ------------ | --------------------------------------------------------------------- | | network | fetch throws (e.g. DNS, timeout) | | http | Response status is not 2xx | | validation | Body/query/response failed schema validation, or response is not JSON |

API reference

defineClient(config)

| Field | Description | | ----------- | --------------------------------------------------------------------------------------- | | baseUrl | Base URL for all requests (new URL(path, baseUrl)). Must be valid for the URL parser. | | errorMode | "result" (default), "throw", or "fallback" | | headers | Default headers for all endpoints | | auth | AuthConfig (bearer / basic / custom) | | endpoints | Map of endpoint definitions |

Endpoint definition

| Field | Description | | ------------- | --------------------------------------------------------------------------- | | method | GET | POST | PUT | PATCH | DELETE | | path | Path starting with / or ""; use :name[string] (etc.) for substitution | | response | Schema for the JSON response body | | body | Optional request body schema (POST / PUT / PATCH only) | | queryParams | Optional typed query schema | | headers | Per-endpoint headers |

Call options

| Field | Description | | -------------- | ---------------------------------------------------- | | params | Path parameters (when the path uses the bracket DSL) | | query | Query string values | | body | JSON body (body methods only) | | headers | Per-call headers | | errorMode | Override client error mode | | defaultValue | Required when using errorMode: "fallback" |

Caveats

  • Path substitution only applies to :name[type] segments; plain :id is left as-is.
  • JSON.stringify for body is not wrapped: circular data or BigInt can cause a rejected promise.
  • For more edge cases and manual QA commands, see .qa/MANUAL_QA.md.

Development (contributors)

bun install
bun run build      # emit dist/
bun run test       # tests/ + ./.qa/live-api.test.ts (live tests skip unless RUN_LIVE_API=1)
bun run qa:manual  # build + manual QA against `import "@nghien-ot/rux"` (see .qa/test.ts)

Compile-time API assertions live in tests/types.test.ts using Vitest expectTypeOf (checked when you pass --typecheck, as in bun run test). For the mandatory TDD workflow and agent roles, see TDD.md and AGENTS.md.

Optional live smoke tests (network): set RUN_LIVE_API=1 and run bun run test (includes .qa/live-api.test.ts; live cases skip unless the env var is set). See MANUAL_QA.md.

License

MIT