1000fetches
v0.2.1
Published
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.
Maintainers
Readme
1000fetches
Schema-powered Fetch 2.0 — where types meet runtime reality
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, retries, streaming, and middleware on top of native fetch.
Highlights
- 🧭 Compile-time path safety —
:pathParamcan't slip through undefined - 🧩 Schema-driven validation — infer types at build time, verify data at runtime
- ⚡ Native streaming — observe, transform, or pipe data chunks as they flow
- 🔁 Retries, timeouts, middleware — production essentials, zero config
- 🎯 Method-based API —
api.get()with schema validation.schema()and extractor.data()for clean and concise code - 🧠 Designed for flow — clear API, predictable behavior, no hidden magic
Quickstart
npm install 1000fetchesimport { 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(),
...
})
// Request data with schema validation
const userResponse = await api
.get('/users/:id', {
pathParams: { id: '123' },
})
.schema(userSchema)
// userResponse.data 👉 { id: number, ... }
// Clean data extraction with full type safety
const user = await api
.get('/users/:id', {
pathParams: { id: '123' },
})
.schema(userSchema)
.data()
// user 👉 { id: number, ... }Or use the default client for quick requests:
import http from '1000fetches'
const user = await http
.get('/users/:id', {
pathParams: { id: '123' },
})
.schema(userSchema)➡️ Key Features
✔️ Type Safety That Actually Works
- Compile-time path validation — TypeScript catches missing
:userIdparameters before runtime - 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 — Exponential backoff with jitter for resilient requests
- Timeout and cancellation support — Built-in
AbortControllerintegration - Structured error handling —
HttpError,NetworkError,TimeoutErrorwith full context - Request/Response middleware — Authentication, logging, transformations
- Minimalistic footprint — Enterprise features without the bloat
✔️ Engineer-Friendly DX
- Method-based API —
api.get(),api.post(),api.put(),api.patch(),api.delete(), and genericapi.request()with full type safety - Schema-first validation — Chain
.schema()for runtime validation and automatic type inference - Smart data extraction — Chain
.data()for direct value access without.dataproperty - Automatic response parsing — JSON/text responses parsed automatically
- Real-time streaming — Access actual data chunks during upload/download
- Zero dependencies — Optional peer dependencies, tree-shakable builds
- TypeScript-first — Full type inference and
IntelliSensesupport
1000fetches combines native fetch performance with enterprise-grade features and bulletproof 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',
onRequestMiddleware: async context => {
const token = await getToken()
context.headers.set('Authorization', `Bearer ${token}`)
return context
},
})Streaming
// Upload progress with actual data chunks
await api.post('/api/files', fileData, {
onUploadStreaming: ({ chunk, transferredBytes, totalBytes }) => {
const progress = Math.round((transferredBytes / totalBytes) * 100)
console.log(`Uploading: ${progress}% - chunk size: ${chunk.length} bytes`)
},
})
// Download progress
await api.get('/api/files/:id', {
pathParams: { id: '123' },
onDownloadStreaming: ({ chunk, transferredBytes, totalBytes }) => {
const progress = Math.round((transferredBytes / totalBytes) * 100)
console.log(`Downloading: ${progress}% - chunk size: ${chunk.length} bytes`)
},
})Error Handling
import {
HttpError,
NetworkError,
TimeoutError,
MiddlewareError,
PathParameterError,
} from '1000fetches'
try {
const user = await api
.get('/users/:id', {
pathParams: { id: '123' },
})
.schema(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 MiddlewareError) {
console.log('Middleware error:', error.message)
} else if (error instanceof PathParameterError) {
console.log('Path parameter error:', error.message)
}
}API Reference
createHttpClient(config?)
Creates a new HTTP client with optional configuration.
function createHttpClient(config?: HttpClientConfig): HttpClientConfiguration Options:
| Option | Type | Description |
| ---------------------- | --------------------------------------------- | ------------------------------- |
| baseUrl | string | Base URL for all requests |
| headers | Record<string, string> | Default headers |
| timeout | number | Default timeout in milliseconds |
| retryOptions | RetryOptions | Default retry configuration |
| onRequestMiddleware | (context: RequestContext) => RequestContext | Request middleware |
| onResponseMiddleware | (response: ResponseType) => ResponseType | Response middleware |
| schemaValidator | SchemaValidator | Custom schema validator |
HTTP Methods
All HTTP methods support schema validation and data extraction:
// GET
const user = await api
.get('/users/:id', {
pathParams: { id: '123' },
})
.schema(userSchema)
.data()
// user 👉 Fully typed user object
// POST
const newUser = await api
.post('/users', userData)
.schema(userSchema)
.data()
// newUser 👉 Fully typed user object
// PUT
const updatedUser = await api
.put('/users/:id', userData, {
pathParams: { id: '123' },
})
.schema(userSchema)
.data()
// updatedUser 👉 Fully typed user object
// PATCH
const patchedUser = await api
.patch('/users/:id', partialData, {
pathParams: { id: '123' },
})
.schema(userSchema)
.data()
// patchedUser 👉 Fully typed user object
// DELETE
await api.delete('/users/:id', {
pathParams: { id: '123' },
})Request Options
| Option | Type | Description |
| --------------------- | ---------------------------------------------------------- | --------------------------------- |
| pathParams | Record<string, string \| number> | Path parameters for URL templates |
| params | Record<string, string \| number \| boolean \| undefined> | Query parameters |
| headers | Record<string, string> | Request headers |
| body | any | Request body |
| timeout | number | Request timeout |
| signal | AbortSignal | Request cancellation signal |
| validateStatus | (status: number) => boolean | Custom status validation |
| responseType | 'text' \| 'blob' \| 'arrayBuffer' | Response type override |
| cache | RequestCache | Cache mode |
| credentials | RequestCredentials | Credentials mode |
| mode | RequestMode | Request mode |
| redirect | RequestRedirect | Redirect mode |
| retryOptions | RetryOptions | Retry configuration |
| onUploadStreaming | (event: UploadStreamingEvent) => void | Upload streaming callback |
| onDownloadStreaming | (event: DownloadStreamingEvent) => void | Download streaming callback |
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: Record<string, string> // Response headers
method: HttpMethod // HTTP method used
url: string // Final URL
raw: Response // Raw fetch Response
}Schema Validation
The library provides a chainable API for schema validation:
// Without schema
const response = await api.get('/users/:id', {
pathParams: { id: '123' },
})
// response 👉 ResponseType<unknown>
// With schema
const response = await api
.get('/users/:id', {
pathParams: { id: '123' },
})
.schema(userSchema)
// response 👉 ResponseType<User>Data Extraction
The method-based API provides clean data extraction with full type safety:
// Extract untyped data (without schema)
const data = await api
.get('/users/:id', {
pathParams: { id: '123' },
})
.data()
// data 👉 unknown
// Direct typed data extraction (with schema)
const user = await api
.get('/users/:id', {
pathParams: { id: '123' },
})
.schema(userSchema)
.data()
// user 👉 { id: number, ... }
// Works with all HTTP methods
const newUser = await api
.post('/users', userData)
.schema(userSchema)
.data()
// newUser 👉 Fully typed user object
const updatedUser = await api
.put('/users/:id', userData, { pathParams: { id: '123' } })
.schema(userSchema)
.data()
// updatedUser 👉 Fully typed user object➡️ Feature Comparison
1000fetches vs Popular Alternatives
| Feature | 1000fetches | Axios | Better-fetch | Up-fetch | Native Fetch | | --------------------- | ----------------- | --------------- | ------------------ | ------------------ | ------------------ | | Bundle Size (gz) | ≈4.3 kB | ≈14.75 kB | ≈3.07 kB | ≈1.6 kB | 0 kB | | TypeScript | ✅ Full inference | ⚠️ Limited | ⚠️ Limited | ✅ Good | ❌ Manual | | Path Params | ✅ Compile-time | ❌ Manual | ❌ Manual | ❌ Manual | ❌ Manual | | Schema Validation | ✅ Multi-library | ❌ None | ⚠️ Limited | ✅ Multi-library | ❌ None | | Retry Logic | ✅ Built-in | ✅ Built-in | ❌ Manual | ✅ Built-in | ❌ Manual | | Error Handling | ✅ Structured | ✅ Good | ⚠️ Basic | ✅ Good | ⚠️ Verbose | | Middleware | ✅ Full support | ✅ Full support | ⚠️ Limited | ✅ Lifecycle hooks | ❌ None | | Streaming | ✅ Real chunks | ❌ None | ❌ None | ✅ Real chunks | ✅ Native | | API Design | ✅ Method-based | ✅ Method-based | ❌ Single function | ❌ Single function | ❌ Single function | | Tree Shaking | ✅ Good | ⚠️ Partial | ✅ Good | ✅ Perfect | ✅ |
📄 License
MIT License - see LICENSE for details.
Built for developers who believe type safety shouldn't be optional. Because your backend deserves skepticism.
