Catalog npm
Back to packageRELEASE DETAILS

0.1.6

npm

1000fetches

A type-first HTTP client with compile-time path validation, schema validation, middleware, retries, and real-time streaming — powered by native fetch. Supports Zod, Valibot, ArkType, and any Standard Schema-compatible library.

apiarktypeaxiosclientfetchhttpschematypescriptvalibotvalidationzod
Published12 Oct 2025
Direct dependencies14
Transitive dependenciesNot calculated

README

Current package documentation

1000fetches

Schema-powered Fetch 2.0 — where types meet runtime reality

npm version TypeScript License: MIT

Built for the 1000th call to be as safe as the first

The problem

You start with fetch, then add a wrapper for error handling. Then generics. Then path params. Then retries and timeouts. Soon every project grows its own version — slightly different, equally fragile.

Alternatives? Tiny helpers that stop halfway, or heavy Axios-style clients that add weight without type guarantees.


The idea

A type-first HTTP client that unifies validation, dynamic defaults, retries, and middleware on top of native fetch.


Highlights

  • 🧭 Compile-time path safety — :pathParam can't slip through undefined
  • 🧩 Schema-driven validation — infer types at build time, verify data at runtime
  • ⚙️ Dynamic defaults — resolve auth, params, timeouts, and fetch options per request
  • 🔁 Retries, timeouts, middleware — production essentials with opt-ins
  • 🎯 Method-based API — api.get() with response contracts .contract() and extractor .data() for clean and concise code
  • 🧠 Designed for flow — clear API, predictable behavior, no hidden magic

Quickstart

npm install 1000fetches
import { createHttpClient } from '1000fetches'
import { z } from 'zod'

// Create client
const api = createHttpClient({
  baseUrl: 'https://api.example.com',
})

// Define schemas for types and safety
const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.email(),
})

// Request data with a response contract
const userResponse = await api
  .get('/users/:id', {
    pathParams: { id: '123' },
  })
  .contract(userSchema)
// userResponse.data 👉 { id: number, ... }

// Clean data extraction with full type safety
const user = await api
  .get('/users/:id', {
    pathParams: { id: '123' },
  })
  .contract(userSchema)
  .data()
// user 👉 { id: number, ... }

With native Node fetch, requests must resolve to absolute URLs. Use an absolute baseUrl in Node, or provide a custom fetch implementation that supports relative URLs.

➡️ Key Features

✔️ Type Safety That Actually Works

  • Compile-time path validation — TypeScript catches missing :userId parameters before runtime
  • Trailing optional path params only — '/users/:id?' supported, '/users/:id?/cards/:cardId' rejected
  • Runtime schema validation — Schemas verify data at the network boundary
  • Schema-based type inference — Types inferred from schemas, no generics needed
  • Multi-schema support — Zod, Valibot, ArkType, or any Standard Schema-compatible library

✔️ Production-Ready Quality

  • Smart retry logic — Opt-in exponential backoff for resilient requests
  • Timeout and cancellation support — Built-in AbortController integration
  • Structured error handling — HttpError, NetworkError, TimeoutError with full context
  • Dynamic defaults and middleware — Authentication, logging, transformations
  • Minimalistic footprint — Production features without the bloat

✔️ Engineer-Friendly DX

  • Method-based API — api.get(), api.post(), api.put(), api.patch(), api.delete(), and generic api.request() with full type safety
  • Contract-first validation — Chain .contract() for runtime validation and automatic type inference
  • Smart data extraction — Chain .data() for direct value access without .data property
  • Automatic response parsing — JSON/text responses parsed automatically
  • Per-request defaults — Resolve auth, params, timeouts, retry settings, and fetch options from request context
  • Tiny runtime footprint — one tiny Standard Schema type dependency, tree-shakable builds
  • TypeScript-first — Full type inference and IntelliSense support

1000fetches combines native fetch performance with production-oriented features and schema-backed type safety.

➡️ Usage Examples

Authentication

const api = createHttpClient({
  baseUrl: 'https://api.example.com',
  headers: { Authorization: `Bearer ${token}` },
})

// Or dynamic auth
const api = createHttpClient({
  baseUrl: 'https://api.example.com',
  defaults: async () => {
    const token = await getToken()
    return {
      headers: { Authorization: `Bearer ${token}` },
    }
  },
})

Dynamic Defaults

const api = createHttpClient({
  baseUrl: 'https://api.example.com',
  defaults: ({ resolvedPath, method }) => ({
    headers: { 'X-Client': 'dashboard' },
    timeout: resolvedPath.startsWith('/reports') ? 60_000 : 10_000,
    retry: method === 'GET',
    params: { locale: 'en-US' },
  }),
})

Middleware

const api = createHttpClient({
  baseUrl: 'https://api.example.com',
  onRequestMiddleware: context => {
    console.log(`→ ${context.method} ${context.url}`)
    return context
  },
  onResponseMiddleware: response => {
    console.log(`← ${response.status} ${response.method} ${response.url}`)
    return response
  },
})

Middleware can observe requests and responses, but it can also mutate them. Keep middleware side-effect-light; use mutation only when dynamic defaults are not enough.

Error Handling

import {
  AbortError,
  HttpError,
  NetworkError,
  TimeoutError,
  MiddlewareError,
  PathParameterError,
  SchemaValidationError,
} from '1000fetches'

try {
  const user = await api
    .get('/users/:id', {
      pathParams: { id: '123' },
    })
    .contract(userSchema)
} catch (error) {
  if (error instanceof HttpError) {
    console.log(`HTTP ${error.status}: ${error.statusText}`)
  } else if (error instanceof NetworkError) {
    console.log('Network error:', error.message)
  } else if (error instanceof TimeoutError) {
    console.log('Request timed out')
  } else if (error instanceof AbortError) {
    console.log('Request was cancelled')
  } else if (error instanceof MiddlewareError) {
    console.log('Middleware error:', error.message)
  } else if (error instanceof PathParameterError) {
    console.log('Path parameter error:', error.message)
  } else if (error instanceof SchemaValidationError) {
    console.log('Schema validation error:', error.message)
  }
}

API Reference

createHttpClient(config?)

Creates a new HTTP client with optional configuration.

function createHttpClient(config?: HttpClientConfig)

Configuration Options:

OptionTypeDescription
baseUrlstringAbsolute or root-relative base URL
headersRecord<string, string>Default headers
timeoutnumberDefault timeout in milliseconds
retryRetryOptions | booleanOpt-in retry configuration
fetchtypeof fetchCustom fetch-compatible transport
serializeBody(body: TBody) => BodyInit | null | undefinedCustom request body serializer
serializeParams(params) => stringCustom query parameter serializer
defaults(context) => RequestDefaults | void | Promise<RequestDefaults | void>Dynamic request defaults
onRequestMiddleware(context: RequestContext) => RequestContext | void | Promise<RequestContext | void>Request middleware
onResponseMiddleware(response: ResponseType) => ResponseType | Promise<ResponseType>Response middleware
schemaValidatorSchemaValidatorCustom schema validator

HTTP Methods

All HTTP methods support response contracts and data extraction:

// GET
const user = await api
  .get('/users/:id', {
    pathParams: { id: '123' },
  })
  .contract(userSchema)
  .data()
// user 👉 Fully typed user object

// POST
const newUser = await api.post('/users', userData).contract(userSchema).data()
// newUser 👉 Fully typed user object

// PUT
const updatedUser = await api
  .put('/users/:id', userData, {
    pathParams: { id: '123' },
  })
  .contract(userSchema)
  .data()
// updatedUser 👉 Fully typed user object

// PATCH
const patchedUser = await api
  .patch('/users/:id', partialData, {
    pathParams: { id: '123' },
  })
  .contract(userSchema)
  .data()
// patchedUser 👉 Fully typed user object

// DELETE
await api.delete('/users/:id', {
  pathParams: { id: '123' },
})

Request Options

OptionTypeDescription
pathParamsRecord<string, string | number | undefined>Path parameters for URL templates
paramsRequestParamsTypeQuery parameters
headersRecord<string, string>Request headers
bodyTBodyRequest body
timeoutnumberRequest timeout
signalAbortSignalRequest cancellation signal
responseType'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData'Response type override
cacheRequestCacheCache mode
credentialsRequestCredentialsCredentials mode
modeRequestModeRequest mode
redirectRequestRedirectRedirect mode
fetchOptionsFetchOptionsNative Fetch/Node passthrough
retryRetryOptions | booleanRetry configuration

Retry Options

Retries are disabled unless you set client retry, request retry: true, or request retry settings. The built-in policy retries idempotent methods by default (GET, HEAD, OPTIONS, PUT, DELETE), honors Retry-After, supports jitter, and never retries one-shot ReadableStream request bodies.

const api = createHttpClient({
  retry: {
    maxRetries: 3,
    retryDelay: 250,
    jitter: 'full',
    onRetry: event => {
      console.log(event.method, event.url, event.nextAttempt)
    },
  },
})

Status and Empty Responses

By default, 1000fetches treats any 2xx response as successful. Declare valid non-2xx protocol flows, such as 304 Not Modified, in response contracts.

Responses that cannot carry a body (204, 205, 304, and HEAD) resolve with data: undefined without attempting JSON parsing.

const response = await api.get('/cached-resource').contract({
  success: {
    default: CachedResourceSchema,
    304: z.undefined(),
  },
})

Response Object

All requests return a ResponseType<T> object:

interface ResponseType<T> {
  data: T // Parsed response data
  status: number // HTTP status code
  statusText: string // HTTP status text
  headers: HttpHeaders // Response headers
  method: HttpMethod
  url: string // Final URL
  raw: Response // Raw fetch Response
}

Response Contracts

The library provides a chainable API for response contracts:

// Without contract
const response = await api.get('/users/:id', {
  pathParams: { id: '123' },
})
// response 👉 ResponseType<unknown>

// With a schema contract
const response = await api
  .get('/users/:id', {
    pathParams: { id: '123' },
  })
  .contract(userSchema)
// response 👉 ResponseType<User>

Contracts can also describe success and error bodies by status:

await api.post('/transactions', transactionInput).contract({
  success: {
    default: CreatedTransactionSchema,
    201: EmptyResponseSchema,
  },
  error: {
    default: ErrorSchema,
    400: BadTransactionResponseSchema,
    409: AlreadyCreatedTransactionResponseSchema,
  },
})

Success status maps accept only 2xx and 3xx status keys. Error status maps accept only 4xx and 5xx status keys. Success map status keys control which non-2xx responses resolve; error contracts validate failed responses before HttpError is thrown.

Error branches validate failed HTTP responses before throwing HttpError with the validated error data. If an error response fails its own contract, 1000fetches throws ContractValidationError with the status, method, URL, response, schema, and original data.

Data Extraction

The method-based API provides clean data extraction with full type safety:

// Extract untyped data (without contract)
const data = await api
  .get('/users/:id', {
    pathParams: { id: '123' },
  })
  .data()
// data 👉 unknown

// Direct typed data extraction (with contract)
const user = await api
  .get('/users/:id', {
    pathParams: { id: '123' },
  })
  .contract(userSchema)
  .data()
// user 👉 { id: number, ... }

// Works with all HTTP methods
const newUser = await api.post('/users', userData).contract(userSchema).data()
// newUser 👉 Fully typed user object

const updatedUser = await api
  .put('/users/:id', userData, { pathParams: { id: '123' } })
  .contract(userSchema)
  .data()
// updatedUser 👉 Fully typed user object

// Run the request and intentionally ignore response data
await api.delete('/users/:id', { pathParams: { id: '123' } }).void()

➡️ Design Trade-Offs

1000fetches is intentionally small, but it is not just a thin fetch alias. It focuses on the features that tend to become repetitive in application code:

CapabilityWhat 1000fetches Provides
TypeScriptSchema-inferred response types and typed path parameters
Path ParamsCompile-time and runtime checks for :param placeholders
Schema ValidationStandard Schema support for Zod, Valibot, ArkType, and more
Retry LogicOpt-in retry policy with backoff, jitter, and Retry-After
Error HandlingStructured errors for HTTP, network, timeout, and validation failures
Dynamic DefaultsPer-request headers, params, timeout, retry, and fetch options
MiddlewareRequest and response middleware with typed context
Tree ShakingSide-effect-free ESM/CJS builds

📄 License

MIT License - see LICENSE for details.


Built for developers who believe type safety shouldn't be optional. Because your backend deserves skepticism.

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Declared dependencies 17

Native requirements
14 direct targetsNot calculated transitive count
Package / targetRequirementRelationship
@standard-schema/spec
Environment
{
  "local_name": "@standard-schema/spec",
  "alias": false,
  "peer_metadata": null
}
^1.0.0runtime
msw
Environment
{
  "local_name": "msw",
  "alias": false,
  "peer_metadata": null
}
2.11.5development
zod
Environment
{
  "local_name": "zod",
  "alias": false,
  "peer_metadata": null
}
4.1.12development
vite
Environment
{
  "local_name": "vite",
  "alias": false,
  "peer_metadata": null
}
7.1.9development
terser
Environment
{
  "local_name": "terser",
  "alias": false,
  "peer_metadata": null
}
5.44.0development
vitest
Environment
{
  "local_name": "vitest",
  "alias": false,
  "peer_metadata": null
}
3.2.4development
arktype
Environment
{
  "local_name": "arktype",
  "alias": false,
  "peer_metadata": null
}
2.1.22development
valibot
Environment
{
  "local_name": "valibot",
  "alias": false,
  "peer_metadata": null
}
1.1.0development
typescript
Environment
{
  "local_name": "typescript",
  "alias": false,
  "peer_metadata": null
}
5.9.3development
@types/node
Environment
{
  "local_name": "@types/node",
  "alias": false,
  "peer_metadata": null
}
22.18.9development
expect-type
Environment
{
  "local_name": "expect-type",
  "alias": false,
  "peer_metadata": null
}
1.2.2development
vite-plugin-dts
Environment
{
  "local_name": "vite-plugin-dts",
  "alias": false,
  "peer_metadata": null
}
4.5.4development
@vitest/coverage-v8
Environment
{
  "local_name": "@vitest/coverage-v8",
  "alias": false,
  "peer_metadata": null
}
3.2.4development
rollup-plugin-visualizer
Environment
{
  "local_name": "rollup-plugin-visualizer",
  "alias": false,
  "peer_metadata": null
}
6.0.4development
zod
Environment
{
  "local_name": "zod",
  "alias": false,
  "peer_metadata": {
    "optional": true
  }
}
^4.0.0peer
arktype
Environment
{
  "local_name": "arktype",
  "alias": false,
  "peer_metadata": {
    "optional": true
  }
}
^2.0.0peer
valibot
Environment
{
  "local_name": "valibot",
  "alias": false,
  "peer_metadata": {
    "optional": true
  }
}
^1.0.0peer

Distribution files

1 collected
Distribution 1

Source distribution

Source file
Checksums from source
{
  "shasum": "2c93d956bf48b09b1fe7bc0f68324034dd44e78d",
  "tarball": "https://registry.npmjs.org/1000fetches/-/1000fetches-0.1.6.tgz",
  "fileCount": 6,
  "integrity": "sha512-KMtR1vO9duqypwiopKhit40951OBktH8u0CpVPriuUEsTrFkV3UmxwIXIlYF3mKQkH9jSAX7qAIHFduunVAtLA==",
  "signatures": [
    {
      "sig": "MEUCIQCa2ZSYNaS6h4/vAqGh8IdMgRag8Hxp4t5yotdtOHlL0AIgZMqmB0u+pMf9oYiXfGvntHcqi8LVUIUdX/3wjO52zOQ=",
      "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"
    }
  ],
  "unpackedSize": 142747
}

Repository

PENDING
your-username/1000fetches

Shared repository data · GitHub

License detected in repository
Not collected

Repository

PENDING
kioviensis/1000fetches

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 17:58 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package_version:5eeeae745c64a9f3a2c14bd004092649f5509b35d0fac019a254518f2d1cd014

Other collected versions

0.3.028 Jun0.2.222 May0.2.125 Oct0.2.020 Oct0.1.1117 Oct0.1.1016 OctView all collected versions →