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

@anmetric/neta

v1.9.9

Published

Tiny, elegant HTTP client built on fetch for browser and Node.js

Downloads

85

Readme

neta

Tiny, elegant HTTP client built on fetch for browser and Node.js

  • Zero dependencies — uses native globalThis.fetch
  • ~12 KB minified, ESM + CJS
  • TypeScript ready — full .d.ts type definitions
  • Works everywhere — browser, Node.js 18+, Deno, Bun

Install

npm install @anmetric/neta

Quick Start

import neta from "@anmetric/neta";

// GET with JSON parsing
const data = await neta.get("https://api.example.com/users").json();

// POST with JSON body
const user = await neta
  .post("https://api.example.com/users", {
    json: { name: "John", email: "[email protected]" },
  })
  .json();

// Direct callable
const res = await neta("https://api.example.com/users", { method: "get" });

API

neta(input, options?)

Returns a ResponsePromise.

input

Type: string | URL | Request

options

Type: object

All fetch options plus:

json

Type: unknown

JSON body. Automatically stringified and sets Content-Type: application/json.

const data = await neta
  .post("https://api.example.com/items", {
    json: { title: "New Item" },
  })
  .json();
searchParams

Type: string | object | URLSearchParams | Array<[string, string]>

Query parameters appended to the URL.

const data = await neta
  .get("https://api.example.com/search", {
    searchParams: { q: "hello", page: 2 },
  })
  .json();
// => GET https://api.example.com/search?q=hello&page=2

Values set to undefined are filtered out.

prefix

Type: string | URL

Prefix prepended to the input URL. Useful for API base paths.

const api = neta.create({ prefix: "https://api.example.com/v2" });

await api.get("users").json();
// => GET https://api.example.com/v2/users
baseUrl

Type: string | URL

Base URL for resolving relative inputs using standard URL resolution.

const api = neta.create({ baseUrl: "https://api.example.com/v2/" });

await api.get("users").json();
// => GET https://api.example.com/v2/users

await api.get("../v1/legacy").json();
// => GET https://api.example.com/v1/legacy
timeout

Type: number | false
Default: 10000 (10 seconds)

Request timeout in milliseconds. Set to false to disable.

totalTimeout

Type: number | false
Default: false

Total timeout across all retries in milliseconds.

await neta.get("https://api.example.com/slow", {
  timeout: 5000,
  totalTimeout: 30000,
  retry: 5,
});
retry

Type: number | object
Default: { limit: 2 }

Retry configuration. Pass a number for simple retry limit, or an object for full control.

// Simple
await neta.get(url, { retry: 3 });

// Full control
await neta.get(url, {
  retry: {
    limit: 3,
    methods: ["get", "put", "head", "delete", "options"],
    statusCodes: [408, 413, 429, 500, 502, 503, 504],
    afterStatusCodes: [413, 429, 503],
    maxRetryAfter: Infinity,
    backoffLimit: Infinity,
    delay: (attemptCount) => 300 * 2 ** (attemptCount - 1),
    jitter: false,
    retryOnTimeout: false,
    shouldRetry: undefined,
  },
});
retry.limit

Type: number
Default: 2

Maximum number of retries.

retry.methods

Type: string[]
Default: ['get', 'put', 'head', 'delete', 'options']

HTTP methods eligible for retry.

retry.statusCodes

Type: number[]
Default: [408, 413, 429, 500, 502, 503, 504]

HTTP status codes that trigger a retry.

retry.afterStatusCodes

Type: number[]
Default: [413, 429, 503]

Status codes where the Retry-After header is honored.

retry.maxRetryAfter

Type: number
Default: Infinity

Maximum Retry-After delay (ms) to accept.

retry.backoffLimit

Type: number
Default: Infinity

Maximum backoff delay (ms).

retry.delay

Type: (attemptCount: number) => number
Default: (n) => 300 * 2 ** (n - 1)

Function returning delay in ms for each attempt.

retry.jitter

Type: boolean | ((delay: number) => number)
Default: false

Add randomness to retry delay to prevent thundering herd.

  • true — random value between 0 and computed delay
  • function — custom jitter function
await neta.get(url, {
  retry: {
    limit: 5,
    delay: (n) => 1000 * 2 ** (n - 1),
    jitter: true,
  },
});
retry.retryOnTimeout

Type: boolean
Default: false

Whether to retry when a request times out.

retry.shouldRetry

Type: ({ error, retryCount }) => boolean | undefined | Promise<boolean | undefined>

Custom function to decide whether to retry. Takes precedence over default checks.

  • Return true to force retry
  • Return false to prevent retry
  • Return undefined to fall through to default behavior
await neta.get(url, {
  retry: {
    limit: 3,
    shouldRetry: ({ error, retryCount }) => {
      if (error.response?.status === 401) return false; // Don't retry auth errors
      return undefined; // Default behavior for others
    },
  },
});
throwHttpErrors

Type: boolean | ((status: number) => boolean)
Default: true

Throw HTTPError for non-2xx responses. Pass a function for custom logic.

// Never throw
const response = await neta.get(url, { throwHttpErrors: false });

// Only throw on 5xx
const response = await neta.get(url, {
  throwHttpErrors: (status) => status >= 500,
});
parseJson

Type: (text: string, context: { request, response }) => unknown

Custom JSON parser. Useful for reviving dates, BigInts, etc.

import LosslessJSON from "lossless-json";

const data = await neta
  .get(url, {
    parseJson: (text) => LosslessJSON.parse(text),
  })
  .json();
stringifyJson

Type: (value: unknown) => string

Custom JSON serializer for the json option.

import LosslessJSON from "lossless-json";

await neta.post(url, {
  json: data,
  stringifyJson: (value) => LosslessJSON.stringify(value),
});
bearerToken

Type: string

Convenience option to set the Authorization: Bearer <token> header. If an Authorization header is already set explicitly, bearerToken will not override it.

const data = await neta
  .get("https://api.example.com/me", {
    bearerToken: "abc123",
  })
  .json();
// Sets header: Authorization: Bearer abc123

// Works with create/extend
const api = neta.create({
  prefix: "https://api.example.com",
  bearerToken: "abc123",
});
context

Type: Record<string, unknown>
Default: {}

Arbitrary data passed through to hooks. Not sent with the request.

await neta.get(url, {
  context: { token: "abc123" },
  hooks: {
    init: [
      (options) => {
        options.headers = {
          ...options.headers,
          Authorization: `Bearer ${options.context.token}`,
        };
      },
    ],
  },
});
fetch

Type: typeof globalThis.fetch

Custom fetch implementation.

import { fetch } from "undici";

const api = neta.create({ fetch });
onDownloadProgress

Type: (progress: { percent, transferredBytes, totalBytes }) => void

Download progress callback. Requires ReadableStream support.

await neta.get("https://example.com/large-file", {
  onDownloadProgress: ({ percent, transferredBytes, totalBytes }) => {
    console.log(
      `${Math.round(percent * 100)}% (${transferredBytes}/${totalBytes})`,
    );
  },
});
onUploadProgress

Type: (progress: { percent, transferredBytes, totalBytes }) => void

Upload progress callback. Requires request streams support (duplex: 'half').

HTTP Method Shortcuts

neta.get(input, options?)
neta.post(input, options?)
neta.put(input, options?)
neta.patch(input, options?)
neta.delete(input, options?)
neta.head(input, options?)
neta.options(input, options?)

ResponsePromise

neta methods return a ResponsePromise — a Promise<Response> with body parsing shortcuts:

const json = await neta.get(url).json();
const text = await neta.get(url).text();
const blob = await neta.get(url).blob();
const buffer = await neta.get(url).arrayBuffer();
const form = await neta.get(url).formData();

.json(schema?)

Parse response as JSON. Optionally validate against a Standard Schema:

import { z } from "zod";

const user = await neta.get("/user/1").json(
  z.object({
    id: z.number(),
    name: z.string(),
  }),
);
// Throws SchemaValidationError if validation fails

Instance Creation

neta.create(defaults?)

Create a new instance with default options:

const api = neta.create({
  prefix: "https://api.example.com",
  bearerToken: "my-token",
  timeout: 30000,
  retry: 3,
});

const data = await api.get("users").json();

neta.extend(defaults?)

Alias for neta.create(). Creates a new instance by extending existing defaults:

const api = neta.create({ prefix: "https://api.example.com" });
const authApi = api.extend({ bearerToken: "my-token" });

Hooks

Five hook points for intercepting the request lifecycle.

hooks.init

Type: Array<(options) => void>

Called synchronously before anything else. Can mutate options directly.

neta.create({
  hooks: {
    init: [
      (options) => {
        options.headers = {
          ...options.headers,
          "X-Request-Id": crypto.randomUUID(),
        };
      },
    ],
  },
});

hooks.beforeRequest

Type: Array<({ request, options, retryCount }) => Request | Response | void>

Called before each request. Return a Request to replace it, a Response to short-circuit, or nothing.

neta.create({
  hooks: {
    beforeRequest: [
      ({ request }) => {
        console.log(`${request.method} ${request.url}`);
      },
    ],
  },
});

hooks.afterResponse

Type: Array<({ request, options, response, retryCount }) => Response | RetryMarker | void>

Called after a successful response. Return a Response to replace it, or neta.retry() to force a retry.

const api = neta.create({
  hooks: {
    afterResponse: [
      async ({ request, response }) => {
        if (response.status === 401) {
          const token = await refreshToken();
          return neta.retry({
            request: new Request(request, {
              headers: {
                ...Object.fromEntries(request.headers),
                Authorization: `Bearer ${token}`,
              },
            }),
          });
        }
      },
    ],
  },
});

hooks.beforeError

Type: Array<({ request, options, error, retryCount }) => Error | void>

Called before an error is thrown. Return an Error to replace it.

neta.create({
  hooks: {
    beforeError: [
      ({ error }) => {
        if (error instanceof HTTPError) {
          error.message = `API Error: ${error.response.status}`;
        }
        return error;
      },
    ],
  },
});

hooks.beforeRetry

Type: Array<({ request, options, error, retryCount }) => Request | Response | symbol | void>

Called before each retry attempt. Return:

  • Request — use this request for the retry
  • Response — skip the retry and use this response
  • stop — abort the retry loop
  • nothing — proceed normally
import { stop } from "@anmetric/neta";

neta.create({
  hooks: {
    beforeRetry: [
      ({ error, retryCount }) => {
        console.log(`Retry #${retryCount}: ${error.message}`);
        if (retryCount > 3) return stop;
      },
    ],
  },
});

Error Handling

HTTPError

Thrown for non-2xx responses (when throwHttpErrors is true).

import { HTTPError } from "@anmetric/neta";

try {
  await neta.get("https://api.example.com/missing");
} catch (error) {
  if (error instanceof HTTPError) {
    console.log(error.response.status); // 404
    console.log(error.data); // Auto-parsed response body
    console.log(error.request); // The Request object
  }
}

TimeoutError

Thrown when a request exceeds the timeout or totalTimeout.

import { TimeoutError } from "@anmetric/neta";

try {
  await neta.get(url, { timeout: 1000 });
} catch (error) {
  if (error instanceof TimeoutError) {
    console.log("Request timed out:", error.request.url);
  }
}

NetworkError

Thrown on network failures (DNS, connection refused, etc.).

import { NetworkError } from "@anmetric/neta";

try {
  await neta.get("https://nonexistent.invalid");
} catch (error) {
  if (error instanceof NetworkError) {
    console.log("Network error:", error.message);
    console.log("Cause:", error.cause);
  }
}

SchemaValidationError

Thrown when JSON response fails schema validation.

import { SchemaValidationError } from "@anmetric/neta";

try {
  await neta.get(url).json(mySchema);
} catch (error) {
  if (error instanceof SchemaValidationError) {
    console.log("Validation issues:", error.issues);
  }
}

TypeScript

neta ships with full TypeScript definitions. Generic type parameters work on .json():

interface User {
  id: number;
  name: string;
}

const user = await neta.get("https://api.example.com/user/1").json<User>();
// user is typed as User

Retry-After Header Support

neta automatically parses these headers during retry:

  • Retry-After
  • RateLimit-Reset
  • X-RateLimit-Retry-After
  • X-RateLimit-Reset
  • X-Rate-Limit-Reset

Supports seconds, timestamps, and HTTP dates.

Browser + Node.js

neta uses globalThis.fetch which is available natively in:

  • All modern browsers
  • Node.js 18+
  • Deno
  • Bun

No polyfills needed.

License

MIT