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

@tknf/typefetcher

v1.0.1

Published

TypeScript-first API client with Standard Schema support, providing excellent DX and strict type safety.

Readme

Github Workflow Status Github npm npm bundle size npm bundle size Github commit activity GitHub last commit Ask DeepWiki

✨ Features

  • 🎯 Type-Safe: Full TypeScript support with strict type inference
  • 📊 Standard Schema: Native support for Zod, Valibot, and other Standard Schema compliant libraries
  • 🔍 Request/Response Validation: Runtime validation with detailed error messages
  • 🏗️ Builder Pattern: Intuitive API inspired by Hono and Octokit
  • 📋 Structured Response: Rich response metadata (headers, status, URL) with ~raw access
  • ⚡ Lightweight: Zero dependencies (except peer dependencies)
  • 🛡️ Error Handling: Comprehensive error types for different failure scenarios
  • 🎪 Flexible: Works with any Standard Schema compliant validation library
  • 🚫 AbortSignal Support: Request cancellation and timeout support

📦 Installation

npm install @tknf/typefetcher

Peer Dependencies

TypeFetcher works with Standard Schema compliant validation libraries. Install one or more:

# Zod (requires v3.25.0+ for Standard Schema support)
npm install zod

# Valibot (requires v1.0.0+ for Standard Schema support)  
npm install valibot

Node.js Compatibility

  • Node.js 18+: Built-in fetch support, works out of the box
  • Node.js < 18: Provide a custom fetch implementation:
# Option 1: node-fetch
npm install node-fetch

# Option 2: undici (fast HTTP client)
npm install undici

🚀 Quick Start

Basic Usage (No Schema)

import { TypeFetcher } from "@tknf/typefetcher";

const client = new TypeFetcher({
  baseURL: "https://jsonplaceholder.typicode.com",
  headers: {
    "Authorization": "Bearer your-token"
  }
});

// Register endpoints
const api = client
  .addEndpoint("GET", "/users")
  .addEndpoint("GET", "/users/{id}")
  .addEndpoint("POST", "/users");

// Make requests (Octokit-style)
const response = await api.request("GET /users");

// Structured response with metadata
console.log("Data:", response.data);           // Response body
console.log("Status:", response.status);       // HTTP status code
console.log("Headers:", response.headers);     // Response headers
console.log("URL:", response.url);             // Request URL
console.log("Raw:", response["~raw"]);         // Raw Response object

// Access specific user
const userResponse = await api.request("GET /users/{id}", {
  params: { id: "1" }
});

const user = userResponse.data; // Just the data
const status = userResponse.status; // 200, 404, etc.

Type-Safe Usage with Zod

import { TypeFetcher } from "@tknf/typefetcher";
import { z } from "zod";

// Define your schemas
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

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

const PathIdSchema = z.object({
  id: z.string()
});

// Create type-safe client
const client = new TypeFetcher({
  baseURL: "https://api.example.com"
});

const api = client
  .addEndpoint("GET", "/users", {
    response: z.array(UserSchema)
  })
  .addEndpoint("GET", "/users/{id}", {
    params: PathIdSchema,           // ✅ params is required when schema provided
    response: UserSchema
  })
  .addEndpoint("POST", "/users", {
    body: CreateUserSchema,         // ✅ body is required when schema provided
    response: UserSchema
  });

// Fully type-safe requests with structured responses
const usersResponse = await api.request("GET /users"); 
// Type: StructuredResponse<User[]>

const users = usersResponse.data; // User[]
const status = usersResponse.status; // number
const headers = usersResponse.headers; // Headers

const userResponse = await api.request("GET /users/{id}", {
  params: { id: "123" } // ✅ TypeScript ensures correct type
});
// Type: StructuredResponse<User>

const user = userResponse.data; // User object
if (userResponse.status === 200) {
  console.log("User found:", user.name);
}

const created = await api.request("POST /users", {
  body: { name: "Jane", email: "[email protected]" } // ✅ Validated at runtime
});

// Access creation details
console.log("Created user:", created.data);
console.log("Location:", created.headers.get("location"));
console.log("Status:", created.status); // 201

📊 Response Structure

Every request returns a structured response with rich metadata:

interface StructuredResponse<T> {
  readonly data: T;              // Parsed response data (your API data)
  readonly headers: Headers;     // Response headers object  
  readonly status: number;       // HTTP status code (200, 404, etc.)
  readonly url: string;          // Final request URL
  readonly "~raw": Response;     // Raw fetch Response object
}

Working with Response Data

const response = await api.request("GET /users/{id}", {
  params: { id: "123" }
});

// Access parsed data (type-safe when schema is provided)
const user = response.data;

// Check HTTP status
if (response.status === 200) {
  console.log("Success!");
} else if (response.status === 404) {
  console.log("User not found");
}

// Access response headers
const contentType = response.headers.get("content-type");
const rateLimit = response.headers.get("x-rate-limit-remaining");

// Get request URL (useful for debugging)
console.log("Request was made to:", response.url);

// Access raw Response for advanced use cases
const rawResponse = response["~raw"];
const responseText = await rawResponse.clone().text();

Type-Safe Usage with Valibot

import { TypeFetcher } from "@tknf/typefetcher";
import * as v from "valibot";

// Define Valibot schemas
const UserSchema = v.object({
  id: v.number(),
  name: v.string(),
  email: v.pipe(v.string(), v.email()),
});

const CreateUserSchema = v.object({
  name: v.string(),
  email: v.pipe(v.string(), v.email()),
});

// Use directly with TypeFetcher
const api = client
  .addEndpoint("GET", "/users", {
    response: v.array(UserSchema)
  })
  .addEndpoint("POST", "/users", {
    body: CreateUserSchema,
    response: UserSchema
  });

// Same type-safe API as with Zod
const usersResponse = await api.request("GET /users");
const users = usersResponse.data; // User[]

const newUserResponse = await api.request("POST /users", {
  body: { name: "John", email: "[email protected]" }
});
const newUser = newUserResponse.data; // User

📚 API Reference

TypeFetcher Constructor

new TypeFetcher(config?: TypeFetcherConfig)

Parameters:

  • config (optional): Configuration object

TypeFetcherConfig:

interface TypeFetcherConfig {
  readonly baseURL?: string;
  readonly headers?: Record<string, string>;
  readonly timeout?: number;
  readonly fetch?: typeof globalThis.fetch;  // Custom fetch implementation
}

addEndpoint

addEndpoint<Method, Path, Schema>(
  method: Method, 
  path: Path, 
  schema?: Schema
): TypeFetcher<...>

Registers a new endpoint with optional schema validation.

Parameters:

  • method: HTTP method ("GET" | "POST" | "PUT" | "PATCH" | "DELETE")
  • path: URL path with optional parameters (e.g., "/users/{id}")
  • schema (optional): Validation schema object

Schema Object:

interface EndpointSchema {
  readonly params?: StandardSchemaV1;    // Path parameters
  readonly query?: StandardSchemaV1;     // Query parameters  
  readonly body?: StandardSchemaV1;      // Request body
  readonly response?: StandardSchemaV1;  // Response validation
}

request

request<K>(key: K, options?: RequestOptions): Promise<StructuredResponse<T>>

Executes a request to a registered endpoint and returns a structured response.

Parameters:

  • key: Endpoint key in format "METHOD /path"
  • options: Request options (automatically typed based on schema)

Request Options:

interface RequestOptions {
  readonly params?: Record<string, string> | SchemaType;   // Path parameters
  readonly query?: Record<string, string> | SchemaType;    // Query parameters
  readonly body?: unknown | SchemaType;                    // Request body
  readonly headers?: Record<string, string>;               // Custom headers
  readonly signal?: AbortSignal;                           // Abort signal
}

// When schema is provided, corresponding fields become required and strongly typed

🔧 Advanced Usage

AbortSignal Support

// Request cancellation
const controller = new AbortController();

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
  const response = await api.request("GET /users/{id}", {
    params: { id: "123" },
    signal: controller.signal
  });
  
  console.log("User:", response.data);
} catch (error) {
  if (error.name === 'AbortError') {
    console.log("Request was cancelled");
  }
}

Custom Headers per Request

const response = await api.request("GET /users/{id}", {
  params: { id: "123" },
  headers: {
    "Accept-Language": "en-US",
    "X-Custom-Header": "value",
    "Authorization": "Bearer specific-token" // Override global headers
  }
});

Query Parameters

const QuerySchema = z.object({
  page: z.string(),
  limit: z.string(),
  search: z.string().optional()
});

const api = client.addEndpoint("GET", "/users", {
  query: QuerySchema,
  response: z.array(UserSchema)
});

const response = await api.request("GET /users", {
  query: {
    page: "1",
    limit: "10",
    search: "john"
  }
});

console.log("Users:", response.data);
console.log("Total pages:", response.headers.get("x-total-pages"));

Working with Raw Response

For advanced use cases, access the raw Response object:

const response = await api.request("GET /download/{id}", {
  params: { id: "file123" }
});

// Access raw Response
const rawResponse = response["~raw"];

// Stream response body
const reader = rawResponse.body?.getReader();
const contentLength = rawResponse.headers.get("content-length");

console.log(`Downloading ${contentLength} bytes`);

// Process stream...
while (reader) {
  const { done, value } = await reader.read();
  if (done) break;
  
  // Process chunk
  console.log(`Received ${value.length} bytes`);
}

Node.js Usage

For Node.js environments, you can provide a custom fetch implementation:

// Node.js 18+ (built-in fetch)
const client = new TypeFetcher({
  baseURL: "https://api.example.com"
});

// Node.js < 18 with node-fetch
import fetch from "node-fetch";
const client = new TypeFetcher({
  baseURL: "https://api.example.com",
  fetch: fetch as unknown as typeof globalThis.fetch
});

// Using undici for better performance
import { fetch } from "undici";
const client = new TypeFetcher({
  baseURL: "https://api.example.com",
  fetch: fetch as unknown as typeof globalThis.fetch
});

Error Handling

import { TypeFetcherError, ValidationError } from "@tknf/typefetcher";

try {
  const response = await api.request("GET /users/{id}", {
    params: { id: "123" }
  });
  
  console.log("User:", response.data);
  console.log("Status:", response.status);
} catch (error) {
  if (error instanceof TypeFetcherError) {
    // HTTP errors (404, 500, etc.)
    console.error(`HTTP ${error.status}: ${error.statusText}`);
    console.error("Response data:", error.data);
  } else if (error instanceof ValidationError) {
    // Schema validation errors
    console.error("Validation failed:", error.message);
    error.issues.forEach(issue => {
      console.error(`- ${issue.message} at ${issue.path?.join('.')}`);
    });
  } else {
    // Other errors (network, abort, etc.)
    console.error("Unexpected error:", error);
  }
}

Schema Transformations

Zod and Valibot schemas with transformations work seamlessly:

const TransformSchema = z.object({
  id: z.string().transform(val => val.toUpperCase()),
  date: z.string().transform(val => new Date(val))
});

const api = client.addEndpoint("GET", "/items/{id}", {
  params: TransformSchema
});

// Input is transformed before making the request
const response = await api.request("GET /items/{id}", {
  params: { id: "abc", date: "2023-01-01" }
  // Becomes: id="ABC", date=Date object in the actual request
});

🌟 Why TypeFetcher?

Standard Schema Native

Unlike other API clients that require adapters or wrappers, TypeFetcher natively supports any Standard Schema compliant library:

// ❌ Other libraries require adapters
const schema = someAdapter(z.string());

// ✅ TypeFetcher uses schemas directly
const schema = z.string(); // Works with Zod 3.25.0+
const schema = v.string(); // Works with Valibot 1.0.0+

Rich Response Information

Get comprehensive response metadata without extra work:

// ❌ Traditional fetch
const rawResponse = await fetch("/api/users");
const data = await rawResponse.json();
// Lost: headers, status, url information

// ✅ TypeFetcher structured response
const response = await api.request("GET /users");
// Available: data, headers, status, url, ~raw

Excellent TypeScript Integration

  • Required Parameters: Schema-specified parameters become required in TypeScript
  • Type Inference: Full type inference from schemas to response types
  • Autocomplete: Rich IDE support with endpoint and parameter suggestions
  • Structured Response: Access both data and metadata with full type safety

Minimal Bundle Size

  • Zero runtime dependencies (except peer dependencies)
  • Tree-shakable exports
  • Only import what you use

🔍 Examples

REST API Client

import { TypeFetcher } from "@tknf/typefetcher";
import { z } from "zod";

const PostSchema = z.object({
  id: z.number(),
  title: z.string(),
  body: z.string(),
  userId: z.number()
});

class BlogAPI {
  private client = new TypeFetcher({
    baseURL: "https://jsonplaceholder.typicode.com"
  });

  private api = this.client
    .addEndpoint("GET", "/posts", {
      response: z.array(PostSchema)
    })
    .addEndpoint("GET", "/posts/{id}", {
      params: z.object({ id: z.string() }),
      response: PostSchema
    })
    .addEndpoint("POST", "/posts", {
      body: z.object({
        title: z.string(),
        body: z.string(),
        userId: z.number()
      }),
      response: PostSchema
    });

  async getAllPosts() {
    const response = await this.api.request("GET /posts");
    return {
      posts: response.data,
      count: response.headers.get("x-total-count")
    };
  }

  async getPost(id: string) {
    const response = await this.api.request("GET /posts/{id}", { 
      params: { id } 
    });
    
    if (response.status === 404) {
      throw new Error("Post not found");
    }
    
    return response.data;
  }

  async createPost(post: { title: string; body: string; userId: number }) {
    const response = await this.api.request("POST /posts", { body: post });
    
    return {
      post: response.data,
      location: response.headers.get("location"),
      status: response.status
    };
  }
}

File Upload with Progress

const api = client.addEndpoint("POST", "/upload", {
  body: z.instanceof(FormData),
  response: z.object({
    fileId: z.string(),
    url: z.string()
  })
});

async function uploadFile(file: File, onProgress?: (progress: number) => void) {
  const formData = new FormData();
  formData.append("file", file);

  const response = await api.request("POST /upload", {
    body: formData,
    headers: {
      // Don't set Content-Type, let browser set it with boundary
    }
  });

  console.log("Upload completed!");
  console.log("File ID:", response.data.fileId);
  console.log("File URL:", response.data.url);
  console.log("Server:", response.headers.get("server"));

  return response.data;
}

Pagination Helper

async function getAllUsers() {
  const users = [];
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await api.request("GET /users", {
      query: { page: page.toString(), limit: "50" }
    });

    users.push(...response.data);

    // Check if there are more pages
    const totalPages = parseInt(response.headers.get("x-total-pages") || "1");
    hasMore = page < totalPages;
    page++;
  }

  return users;
}

🛠️ Development

# Install dependencies
pnpm install

# Run tests
pnpm test

# Run tests with coverage
pnpm run test:coverage

# Type checking
pnpm run typecheck

# Linting
pnpm run lint

# Build
pnpm run build

📋 Requirements

  • Node.js: 16.x or higher
  • TypeScript: 5.x or higher
  • Zod: 3.25.0+ (if using Zod)
  • Valibot: 1.0.0+ (if using Valibot)

📄 License

MIT License - see LICENSE for details.

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Ensure tests pass: pnpm run test
  5. Ensure linting passes: pnpm run lint
  6. Submit a pull request

👏 Acknowledgments