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

ty-fetch

v0.3.2

Published

Automatic TypeScript types for any REST API. No codegen, no manual types — just fetch.

Readme

npm version license CI Try it

A tiny, zero-dependency HTTP client that auto-discovers your OpenAPI specs and types your API calls on-the-fly. No codegen, no build step — types generated by a TS plugin. Try it in your browser →

npm install ty-fetch
// tsconfig.json
{
  "compilerOptions": {
    "plugins": [{
      "name": "ty-fetch/plugin",
      // Optional — point to specs manually. Without this, ty-fetch
      // auto-discovers specs at /openapi.json, /.well-known/openapi.yaml, etc.
      "specs": {
        "api.mycompany.com": "https://api.mycompany.com/docs/openapi.json"
      }
    }]
  }
}
import ty from "ty-fetch";

// Fully typed — response, body, path params, query params, headers
const { data, error } = await ty.post("https://api.mycompany.com/v1/users/{team}/invite", {
  params: {
    path: { team: "engineering" },
    query: { notify: true },
  },
  body: { email: "[email protected]", role: "admin" },
  headers: { "x-api-key": process.env.API_KEY },
});

if (error) return console.error(error);
console.log(data.user.id); // fully typed, autocomplete works

If your API serves an OpenAPI spec at /openapi.json (or any well-known path), ty-fetch finds it automatically. No config needed.


🤔 How does it work?

ty-fetch is a TypeScript language service plugin. When you write a ty.get("https://...") call:

  1. 🔍 It extracts the domain from the URL
  2. 📡 Auto-discovers the OpenAPI spec (checks /openapi.json, /.well-known/openapi.yaml, etc.)
  3. 🏗️ Generates typed overloads on-the-fly — response types, query params, headers, everything
  4. ✅ Validates your API paths and suggests corrections for typos

Types appear in your editor instantly. When the spec changes, types update automatically. No build step ever.

Compared to other tools

| | ty-fetch | openapi-fetch | orval | |---|---|---|---| | Codegen step | None | npx openapi-typescript first | npx orval | | Generated files | None | .d.ts type files | Full client code | | Spec changes | Auto-updates | Re-run codegen | Re-run codegen | | Auto-discovery | Probes well-known paths | Manual | Manual | | Editor integration | TS plugin (autocomplete, hover, path validation) | Types only | Types only | | Path validation | Typo detection with "did you mean?" | None | None | | Streaming | SSE, NDJSON built-in | parseAs: "stream" | Depends on client | | Runtime | ~100 LOC, zero deps | ~6 KB | Axios/fetch client |


📦 Quick start

1. Install

npm install ty-fetch

2. Add the plugin to tsconfig.json

{
  "compilerOptions": {
    "plugins": [{ "name": "ty-fetch/plugin" }]
  }
}

3. Start fetching

import ty from "ty-fetch";

// If your API has a spec at /openapi.json — types just work
const { data, error } = await ty.get("https://api.mycompany.com/v1/users");

That's it. ✨

VS Code users: Make sure you're using the workspace TypeScript version, not the built-in one. Command Palette → TypeScript: Select TypeScript VersionUse Workspace Version

Want to try it without setting up a project? Open in GitHub Codespaces → — types work instantly, no local setup needed.


🔍 Spec discovery

Auto-discovery (zero config)

When you call ty.get("https://api.example.com/..."), ty-fetch automatically probes the domain for an OpenAPI spec at these well-known paths:

/.well-known/openapi.json    /.well-known/openapi.yaml
/openapi.json                /openapi.yaml
/api/openapi.json            /docs/openapi.json
/swagger.json                /api-docs/openapi.json

If any path returns a valid OpenAPI spec, types are generated automatically.

This means if your internal API serves a spec, ty-fetch will find it with zero configuration.

Point to specific specs

For APIs that don't serve specs at standard paths, or for local spec files:

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "ty-fetch/plugin",
        "specs": {
          // Remote spec URL
          "api.mycompany.com": "https://api.mycompany.com/docs/v2/openapi.json",

          // Local file (resolved relative to tsconfig)
          "payments.internal.com": "./specs/payments.yaml",

          // Third-party API
          "api.partner.com": "https://partner.com/openapi.json"
        }
      }
    ]
  }
}

Supported spec formats:

| Format | Versions | |---|---| | OpenAPI | 3.0, 3.1 | | Swagger | 2.0 | | File types | JSON, YAML | | Sources | Local files, remote URLs, auto-discovered |

Custom specs override auto-discovery for the same domain.


📖 API Reference

import ty from "ty-fetch"

The default export is a pre-configured TyFetch instance.

HTTP Methods

ty.get(url, options?)      // GET
ty.post(url, options?)     // POST
ty.put(url, options?)      // PUT
ty.patch(url, options?)    // PATCH
ty.delete(url, options?)   // DELETE
ty.head(url, options?)     // HEAD
ty(url, options?)          // Custom method (set options.method)

All methods return Promise<{ data, error, response }>.

When the plugin is active, the url parameter and all options are typed from the OpenAPI spec. Without the plugin, everything still works — just untyped.

Response Shape

Every method returns { data, error, response }:

const { data, error, response } = await ty.get("https://api.example.com/v1/users");

if (error) {
  // error is the parsed error body (typed if spec defines error responses)
  console.error(error.message);
  console.log(response.status); // raw Response always available
  return;
}

// data is the parsed response body — fully typed from the spec
console.log(data.users);
  • data — parsed response body (undefined if error). JSON is auto-parsed, otherwise text.
  • error — parsed error body on non-2xx responses (undefined if success)
  • response — the raw Response object (always present)

No .json() call needed — responses are parsed automatically.

Error Handling

ty-fetch uses a discriminated union — check error to narrow the type:

const { data, error, response } = await ty.get("https://api.example.com/v1/users/123");

if (error) {
  // data is undefined here, error is the parsed response body
  console.log(response.status);  // 404, 422, 500, etc.
  console.log(error);            // parsed JSON from the API
  return;
}

// data is typed here, error is undefined
console.log(data.name);

What error contains:

| Scenario | error value | |---|---| | API returns JSON error body | The parsed JSON (e.g. { message: "Not found", code: "NOT_FOUND" }) | | API returns non-JSON error | { message: "<statusText>" } | | Network failure / DNS error | fetch() throws — not caught by ty-fetch (see below) |

The shape of error depends on your API. Most APIs return something like { message, code } or { error: { message } }, but ty-fetch gives you whatever the server sent:

const { error, response } = await ty.post("https://api.example.com/v1/users", {
  body: { email: "invalid" },
});

if (error) {
  // Status-based handling
  if (response.status === 422) {
    // error is whatever your API returns for validation errors
    console.log(error.errors); // e.g. [{ field: "email", message: "Invalid format" }]
  } else if (response.status === 401) {
    redirectToLogin();
  }
}

Network errors (DNS failure, timeout, no internet) are not API responses — fetch() itself throws. Wrap the call in try/catch if you need to handle these:

try {
  const { data, error } = await ty.get("https://api.example.com/v1/users");
  if (error) {
    // API responded with non-2xx — server is reachable
  }
} catch (e) {
  // Network-level failure — server is unreachable
}

Streaming errors work differently — ty.stream() throws an HTTPError on non-2xx responses:

try {
  for await (const event of ty.stream("https://api.example.com/v1/events")) {
    console.log(event);
  }
} catch (e) {
  if (e instanceof ty.HTTPError) {
    console.log(e.response.status); // the raw Response is attached
  }
}

Options

ty.post("https://api.example.com/v1/users/{team}/invite", {
  // Path params — replaces {placeholders} in the URL
  params: {
    path: { team: "engineering" },
    query: { notify: true, role: "admin" },
  },

  // JSON request body (auto-serialized, Content-Type set automatically)
  body: { email: "[email protected]", name: "Jane Doe" },

  // Headers (typed from security schemes when plugin is active)
  headers: { "x-api-key": "sk_live_..." },

  // Prefix URL — prepended to the url argument
  prefixUrl: "https://api.example.com",

  // All standard fetch options are supported
  signal: AbortSignal.timeout(5000),
  cache: "no-store",
  credentials: "include",
});

| Option | Type | Description | |---|---|---| | body | object | JSON body — auto-serialized, Content-Type: application/json set | | params.path | object | Replaces {placeholder} segments in the URL | | params.query | object | Appended as ?key=value query string | | headers | object | HTTP headers (typed from spec security schemes) | | prefixUrl | string | Prepended to the URL (useful with create/extend) |

Plus all standard RequestInit options (signal, cache, credentials, mode, etc.)

Creating Instances

// Create a pre-configured instance
const api = ty.create({
  prefixUrl: "https://api.mycompany.com",
  headers: { "x-api-key": process.env.API_KEY },
});

// Now use short paths
const { data } = await api.get("/v1/users");
const { data: user } = await api.post("/v1/users", {
  body: { name: "Jane" },
});

// Extend an existing instance (merges options)
const adminApi = api.extend({
  headers: { "x-admin-token": process.env.ADMIN_TOKEN },
});

Middleware

Add middleware to intercept requests and responses:

import ty from "ty-fetch";

// Add auth header to every request
ty.use({
  onRequest(request) {
    request.headers.set("Authorization", `Bearer ${getToken()}`);
    return request;
  },
});

// Log all responses
ty.use({
  onResponse(response) {
    console.log(`${response.status} ${response.url}`);
    return response;
  },
});

// Retry on 401
ty.use({
  async onResponse(response) {
    if (response.status === 401) {
      await refreshToken();
      return fetch(response.url, { headers: { Authorization: `Bearer ${getToken()}` } });
    }
    return response;
  },
});

| Hook | Signature | Description | |---|---|---| | onRequest | (request: Request) => Request \| RequestInit \| void | Modify the request before it's sent | | onResponse | (response: Response) => Response \| void | Modify or replace the response |

Both hooks can be async. Middleware runs in the order it's added.

Streaming

Stream SSE (Server-Sent Events), NDJSON, or raw text responses:

// Server-Sent Events (e.g. OpenAI, Anthropic streaming APIs)
for await (const event of ty.stream("https://api.example.com/v1/chat", {
  method: "POST",
  body: { prompt: "Hello", stream: true },
})) {
  console.log(event); // each parsed SSE event
}

// NDJSON streaming
for await (const line of ty.stream("https://api.example.com/v1/logs")) {
  console.log(line); // each parsed JSON line
}

Auto-detects the format from Content-Type:

  • text/event-stream → SSE (parses data: lines, stops at [DONE])
  • application/x-ndjson / application/jsonl → NDJSON (parses each line as JSON)
  • Anything else → raw text chunks

Plugin Features (editor only)

When the TS plugin is active, you get these extras on top of the runtime API:

| Feature | What it does | |---|---| | Typed responses | data is the actual response type from the spec, not any | | Typed body | body option is validated against the spec's request body schema | | Typed query params | params.query keys and types from the spec's parameter definitions | | Typed path params | params.path keys from {placeholder} segments | | Typed headers | Required headers from the spec's security schemes | | Path validation | Red squiggles on invalid API paths with "did you mean?" | | Autocomplete | URL completions inside string literals, filtered by HTTP method | | Hover docs | Hover over a URL to see available methods and descriptions | | JSDoc | Property descriptions from the spec appear in hover tooltips | | Example inference | Types inferred from response example when schema is missing |


🖥️ CI / Type checking

Why not just tsc?

tsc doesn't run TypeScript language service plugins — it only sees the base any types. Your code will compile, but you won't get type errors for wrong API paths or mismatched params.

For CI, you have two options:

Option 1: ty-fetch CLI (recommended)

Validates API paths against OpenAPI specs — catches typos and invalid endpoints:

npx ty-fetch                    # uses ./tsconfig.json
npx ty-fetch tsconfig.json      # explicit path
npx ty-fetch --verbose          # show spec fetching details
src/api.ts:21:11 - error TF99001: Path '/v1/uusers' does not exist.
                   Did you mean '/v1/users'?

1 error(s) found.

Add to your CI pipeline:

# GitHub Actions
- run: npx ty-fetch tsconfig.json

Option 2: ESLint plugin (coming soon)

If you prefer eslint over a separate CLI, you can use the ty-fetch eslint rule:

npm install -D eslint-plugin-ty-fetch
// eslint.config.mjs
import tyFetch from "eslint-plugin-ty-fetch";

export default [tyFetch.configs.recommended];

This runs the same validation as the CLI but inside your existing eslint pipeline.

Note: The eslint plugin is not yet published. For now, use the CLI.


🌍 Runtime compatibility

ty-fetch is a thin wrapper around the standard Fetch API. It works anywhere fetch is available:

| Runtime | Supported | |---|---| | Node.js | 18+ (native fetch) | | Bun | ✅ | | Deno | ✅ | | Browsers | ✅ (all modern browsers) | | Cloudflare Workers | ✅ |

The TS plugin (type generation) runs in your editor's TypeScript server — it doesn't affect runtime behavior.


❓ FAQ

The TS plugin needs to be active. Check:

  1. Plugin is in tsconfig.json under compilerOptions.plugins
  2. In VS Code, you're using the workspace TypeScript version (not the built-in one)
  3. Restart the TS server after config changes (Command Palette → "TypeScript: Restart TS Server")
  4. The API's OpenAPI spec is reachable (try curl https://your-api.com/openapi.json)

tsc doesn't run language service plugins — it only sees the base any types. Use the ty-fetch CLI for CI validation: npx ty-fetch tsconfig.json

Yes. The runtime client works independently — you get { data, error, response } back from every call. You just won't get typed responses or path validation. It's a perfectly usable HTTP client on its own.

You can point to a local spec file in your tsconfig:

"specs": { "api.mycompany.com": "./specs/my-api.yaml" }

The runtime client (index.js) is ~100 lines. The plugin code ships in dist/ but only runs inside the TS server, not in your bundle.


🧪 Development

npm run build          # compile TypeScript
npm run watch          # compile in watch mode
npm test               # run unit tests (132 tests)
npm run check          # lint with eslint

License

MIT