Observed since 20 Oct, 17:58 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
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.
Schema-powered Fetch 2.0 — where types meet runtime reality
Built for the 1000th call to be as safe as the first
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.
A type-first HTTP client that unifies validation, dynamic defaults, retries, and middleware on top of native fetch.
:pathParam can't slip through undefinedapi.get() with response contracts .contract() and extractor .data() for clean and concise codenpm 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.
:userId parameters before runtime'/users/:id?' supported, '/users/:id?/cards/:cardId' rejectedAbortController integrationHttpError, NetworkError, TimeoutError with full contextapi.get(), api.post(), api.put(), api.patch(), api.delete(), and generic api.request() with full type safety.contract() for runtime validation and automatic type inference.data() for direct value access without .data propertyIntelliSense support1000fetches combines native fetch performance with production-oriented features and schema-backed type safety.
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}` },
}
},
})
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' },
}),
})
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.
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)
}
}
Creates a new HTTP client with optional configuration.
function createHttpClient(config?: HttpClientConfig)
Configuration Options:
| Option | Type | Description |
|---|---|---|
baseUrl | string | Absolute or root-relative base URL |
headers | Record<string, string> | Default headers |
timeout | number | Default timeout in milliseconds |
retry | RetryOptions | boolean | Opt-in retry configuration |
fetch | typeof fetch | Custom fetch-compatible transport |
serializeBody | (body: TBody) => BodyInit | null | undefined | Custom request body serializer |
serializeParams | (params) => string | Custom 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 |
schemaValidator | SchemaValidator | Custom schema validator |
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' },
})
| Option | Type | Description |
|---|---|---|
pathParams | Record<string, string | number | undefined> | Path parameters for URL templates |
params | RequestParamsType | Query parameters |
headers | Record<string, string> | Request headers |
body | TBody | Request body |
timeout | number | Request timeout |
signal | AbortSignal | Request cancellation signal |
responseType | 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData' | Response type override |
cache | RequestCache | Cache mode |
credentials | RequestCredentials | Credentials mode |
mode | RequestMode | Request mode |
redirect | RequestRedirect | Redirect mode |
fetchOptions | FetchOptions | Native Fetch/Node passthrough |
retry | RetryOptions | boolean | Retry configuration |
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)
},
},
})
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(),
},
})
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
}
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.
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()
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:
| Capability | What 1000fetches Provides |
|---|---|
| TypeScript | Schema-inferred response types and typed path parameters |
| Path Params | Compile-time and runtime checks for :param placeholders |
| Schema Validation | Standard Schema support for Zod, Valibot, ArkType, and more |
| Retry Logic | Opt-in retry policy with backoff, jitter, and Retry-After |
| Error Handling | Structured errors for HTTP, network, timeout, and validation failures |
| Dynamic Defaults | Per-request headers, params, timeout, retry, and fetch options |
| Middleware | Request and response middleware with typed context |
| Tree Shaking | Side-effect-free ESM/CJS builds |
MIT License - see LICENSE for details.
Built for developers who believe type safety shouldn't be optional. Because your backend deserves skepticism.
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
@standard-schema/specEnvironment{
"local_name": "@standard-schema/spec",
"alias": false,
"peer_metadata": null
} | ^1.0.0 | runtime |
mswEnvironment{
"local_name": "msw",
"alias": false,
"peer_metadata": null
} | 2.11.5 | development |
zodEnvironment{
"local_name": "zod",
"alias": false,
"peer_metadata": null
} | 4.1.12 | development |
viteEnvironment{
"local_name": "vite",
"alias": false,
"peer_metadata": null
} | 7.1.9 | development |
terserEnvironment{
"local_name": "terser",
"alias": false,
"peer_metadata": null
} | 5.44.0 | development |
vitestEnvironment{
"local_name": "vitest",
"alias": false,
"peer_metadata": null
} | 3.2.4 | development |
arktypeEnvironment{
"local_name": "arktype",
"alias": false,
"peer_metadata": null
} | 2.1.22 | development |
valibotEnvironment{
"local_name": "valibot",
"alias": false,
"peer_metadata": null
} | 1.1.0 | development |
typescriptEnvironment{
"local_name": "typescript",
"alias": false,
"peer_metadata": null
} | 5.9.3 | development |
@types/nodeEnvironment{
"local_name": "@types/node",
"alias": false,
"peer_metadata": null
} | 22.18.9 | development |
expect-typeEnvironment{
"local_name": "expect-type",
"alias": false,
"peer_metadata": null
} | 1.2.2 | development |
vite-plugin-dtsEnvironment{
"local_name": "vite-plugin-dts",
"alias": false,
"peer_metadata": null
} | 4.5.4 | development |
@vitest/coverage-v8Environment{
"local_name": "@vitest/coverage-v8",
"alias": false,
"peer_metadata": null
} | 3.2.4 | development |
rollup-plugin-visualizerEnvironment{
"local_name": "rollup-plugin-visualizer",
"alias": false,
"peer_metadata": null
} | 6.0.4 | development |
zodEnvironment{
"local_name": "zod",
"alias": false,
"peer_metadata": {
"optional": true
}
} | ^4.0.0 | peer |
arktypeEnvironment{
"local_name": "arktype",
"alias": false,
"peer_metadata": {
"optional": true
}
} | ^2.0.0 | peer |
valibotEnvironment{
"local_name": "valibot",
"alias": false,
"peer_metadata": {
"optional": true
}
} | ^1.0.0 | peer |
Source distribution
{
"shasum": "bee82ec9c9aac2bb4fcdbc2d1961b1e5c9896da3",
"tarball": "https://registry.npmjs.org/1000fetches/-/1000fetches-0.1.7.tgz",
"fileCount": 6,
"integrity": "sha512-D4ZhJi4Wtq7laJjCB6SOrJG7+nOBeC7zJnrRDh+tAJ853SFvMC3DwKvh/jiqKyw7BXTeT3Ac+5S+VSTEOn/1ZQ==",
"signatures": [
{
"sig": "MEYCIQDfX+cqjYhGkswOBcW37un+H0iCrTTF/8OoEKqysOt80wIhAIAB5b0QrBfLwoJyWA1L5T1XUMYCV0iJ1RpKX779u1sy",
"keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"
}
],
"unpackedSize": 141984
}Observed since 20 Oct, 17:58 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:fceb159035d4facfd57dde36ca1850a49ca2468a7b783d2a0d1ae36b6fc5acb9