upfetch - advanced fetch client builder
_upfetch_ is an advanced fetch client builder with standard schema validation,
automatic response parsing, smart defaults and more. Designed to make data fetching
type-safe and developer-friendly while keeping the familiar fetch API.
[中文文档 (AI 翻译)](./README_ZH.md)
## Table of Contents
- [Highlights](#️-highlights)
- [Agent Skill](#️-agent-skill)
- [QuickStart](#️-quickstart)
- [Key Features](#️-key-features)
- [Request Configuration](#️-request-configuration)
- [Simple Query Parameters](#️-simple-query-parameters)
- [Automatic Body Handling](#️-automatic-body-handling)
- [Schema Validation](#️-schema-validation)
- [Lifecycle Hooks](#️-lifecycle-hooks)
- [Timeout](#️-timeout)
- [Retry](#️-retry)
- [Error Handling](#️-error-handling)
- [Usage](#️-usage)
- [Authentication](#️-authentication)
- [Delete a default option](#️-delete-a-default-option)
- [FormData](#️-formdata)
- [Multiple fetch clients](#️-multiple-fetch-clients)
- [Streaming](#️-streaming)
- [Progress](#️-progress)
- [Advanced Usage](#️-advanced-usage)
- [Error as value](#️-error-as-value)
- [Custom response parsing](#️-custom-response-parsing)
- [Custom response errors](#️-custom-response-errors)
- [Custom params serialization](#️-custom-params-serialization)
- [Custom body serialization](#️-custom-body-serialization)
- [Defaults based on the request](#️-defaults-based-on-the-request)
- [API Reference](#️-api-reference)
- [Feature Comparison](#️-feature-comparison)
- [Environment Support](#️-environment-support)
## ➡️ Highlights
- **Lightweight** - 1.6kB gzipped, no dependency
- **Typesafe** - Validate API responses with [zod][zod], [valibot][valibot] or [arktype][arktype]
- ️ **Practical API** - Use objects for `params` and `body`, get parsed responses automatically
- **Flexible Config** - Set defaults like `baseUrl` or `headers` once, use everywhere
- **Comprehensive** - Built-in retries, timeouts, progress tracking, streaming, lifecycle hooks, and more
- **Familiar** - same API as fetch with additional options and sensible defaults
## ➡️ Agent Skill
Install the `up-fetch` skill with:
```bash
npx skills add L-Blondy/up-fetch
```
## ➡️ QuickStart
```bash
npm i up-fetch
```
Create a new upfetch instance:
```ts
import { up } from 'up-fetch'
export const upfetch = up(fetch)
```
Make a fetch request with schema validation:
```ts
import { upfetch } from './upfetch'
import { z } from 'zod'
const user = await upfetch('https://a.b.c/users/1', {
schema: z.object({
id: z.number(),
name: z.string(),
avatar: z.string().url(),
}),
})
```
The response is already **parsed** and properly **typed** based on the schema.
_upfetch_ extends the native fetch API, which means all standard fetch options are available.
## ➡️ Key Features
### ✔️ Request Configuration
Set defaults for all requests when creating an instance:
```ts
const upfetch = up(fetch, () => ({
baseUrl: 'https://a.b.c',
timeout: 30000,
}))
```
Check out the the [API Reference][api-reference] for the full list of options.
### ✔️ Simple Query Parameters
With raw fetch:
```ts
fetch(
`https://api.example.com/todos?search=${search}&skip=${skip}&take=${take}`,
)
```
With _upfetch_:
```ts
upfetch('/todos', {
params: { search, skip, take },
})
```
Use the [serializeParams][api-reference] option to customize the query parameter serialization.
### ✔️ Automatic Body Handling
With raw fetch:
```ts
fetch('https://api.example.com/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'New Todo' }),
})
```
With _upfetch_:
```ts
upfetch('/todos', {
method: 'POST',
body: { title: 'New Todo' },
})
```
_upfetch_ also supports all [fetch body types](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#body).
Check out the [serializeBody][api-reference] option to customize the body serialization.
### ✔️ Schema Validation
Since _upfetch_ follows the [Standard Schema Specification][standard-schema] it can be used with any schema library that implements the spec. \
See the full list [here][standard-schema-libs].
With **zod** 3.24+
```ts
import { z } from 'zod'
const posts = await upfetch('/posts/1', {
schema: z.object({
id: z.number(),
title: z.string(),
}),
})
```
With **valibot** 1.0+
```ts
import { object, string, number } from 'valibot'
const posts = await upfetch('/posts/1', {
schema: object({
id: number(),
title: string(),
}),
})
```
### ✔️ Lifecycle Hooks
Control request/response lifecycle with simple hooks:
```ts
const upfetch = up(fetch, () => ({
onRequest: (options) => {
// Called before the request is made, options might be mutated here
},
onSuccess: (data, options) => {
// Called when the request successfully completes
},
onError: (error, options) => {
// Called when the request fails
},
}))
```
### ✔️ Timeout
Set a timeout for one request:
```ts
upfetch('/todos', {
timeout: 3000,
})
```
Set a default timeout for all requests:
```ts
const upfetch = up(fetch, () => ({
timeout: 5000,
}))
```
### ✔️ Retry
The retry functionality allows you to automatically retry failed requests with configurable attempts, delay, and condition.
```ts
const upfetch = up(fetch, () => ({
retry: {
attempts: 3,
delay: 1000,
},
}))
```
Examples:
Per-request retry config
```ts
await upfetch('/api/data', {
method: 'DELETE',
retry: {
attempts: 2,
},
})
```
Exponential retry delay
```ts
const upfetch = up(fetch, () => ({
retry: {
attempts: 3,
delay: (ctx) => ctx.attempt ** 2 * 1000,
},
}))
```
Retry based on the request method
```ts
const upfetch = up(fetch, () => ({
retry: {
// One retry for GET requests, no retries for other methods:
attempts: (ctx) => (ctx.request.method === 'GET' ? 1 : 0),
delay: 1000,
},
}))
```
Retry based on the response status
```ts
const upfetch = up(fetch, () => ({
retry: {
when({ response }) {
if (!response) return false
return [408, 413, 429, 500, 502, 503, 504].includes(response.status)
},
attempts: 1,
delay: 1000,
},
}))
```
Retry on network errors, timeouts, or any other error
```ts
const upfetch = up(fetch, () => ({
retry: {
attempts: 2,
delay: 1000,
when: (ctx) => {
// Retry on timeout errors
if (ctx.error) return ctx.error.name === 'TimeoutError'
// Retry on 429 server errors
if (ctx.response) return ctx.response.status === 429
return false
},
},
}))
```
### ✔️ Error Handling
#### ResponseError
Raised when `response.ok` is `false`. \
Use `isResponseError` to identify this error type.
```ts
import { isResponseError } from 'up-fetch'
try {
await upfetch('/todos/1')
} catch (error) {
if (isResponseError(error)) {
console.log(error.status)
}
}
```
- Use the [parseRejected][api-reference] option to throw a custom error instead.
- Use the [reject][api-reference] option to decide **when** to throw.
#### ResponseValidationError
Raised when schema validation fails. \
Use `isResponseValidationError` to identify this error type.
```ts
import { isResponseValidationError } from 'up-fetch'
try {
await upfetch('/todos/1', { schema: todoSchema })
} catch (error) {
if (isResponseValidationError(error)) {
console.log(error.issues)
}
}
```
## ➡️ Usage
### ✔️ Authentication
You can easily add authentication to all requests by setting a default header.
Retrieve the token from `localStorage` before each request:
```ts
const upfetch = up(fetch, () => ({
headers: { Authorization: localStorage.getItem('bearer-token') },
}))
```
Retrieve an async token:
```ts
const upfetch = up(fetch, async () => ({
headers: { Authorization: await getToken() },
}))
```
### ✔️ Delete a default option
Simply pass `undefined`:
```ts
upfetch('/todos', {
signal: undefined,
})
```
Also works for single `params` and `headers`:
```ts
upfetch('/todos', {
headers: { Authorization: undefined },
})
```
### ✔️ FormData
Grab the FormData from a `form`.
```ts
const form = document.querySelector('#my-form')
upfetch('/todos', {
method: 'POST',
body: new FormData(form),
})
```
Or create FormData from an object:
```ts
import { serialize } from 'object-to-formdata'
const upfetch = up(fetch, () => ({
serializeBody: (body) => serialize(body),
}))
upfetch('https://a.b.c', {
method: 'POST',
body: { file: new File(['foo'], 'foo.txt') },
})
```
### ✔️ Multiple fetch clients
You can create multiple upfetch instances with different defaults:
```ts
const fetchMovie = up(fetch, () => ({
baseUrl: 'https://api.themoviedb.org',
headers: {
accept: 'application/json',
Authorization: `Bearer ${process.env.API_KEY}`,
},
}))
const fetchFile = up(fetch, () => ({
parseResponse: async (res) => {
const name = res.url.split('/').at(-1) ?? ''
const type = res.headers.get('content-type') ?? ''
return new File([await res.blob()], name, { type })
},
}))
```
### ✔️ Streaming
_upfetch_ provides powerful streaming capabilities through `onRequestStreaming` for upload operations, and `onResponseStreaming` for download operations.
Both handlers receive the following properties:
- `chunk: Uint8Array`: The current chunk of data being streamed
- `transferredBytes: number`: The amount of data transferred so far
- `totalBytes?: number`: The total size of the data, read from the `"Content-Length"` header. \
For request streaming, if the header is not present, totalBytes are read from the request body.
Here's an example of processing a streamed response from an AI chatbot:
```ts
const decoder = new TextDecoder()
upfetch('/ai-chatbot', {
onResponseStreaming: ({ chunk }) => {
const text = decoder.decode(chunk, { stream: true })
console.log(text)
},
})
```
### ✔️ Progress
#### Upload progress:
```ts
upfetch('/upload', {
method: 'POST',
body: new File(['large file'], 'foo.txt'),
onRequestStreaming: ({ transferredBytes, totalBytes }) => {
console.log(`Progress: ${transferredBytes} / ${totalBytes}`)
},
})
```
#### Download progress:
```ts
upfetch('/download', {
onResponseStreaming: ({
transferredBytes,
totalBytes = transferredBytes,
}) => {
console.log(`Progress: ${transferredBytes} / ${totalBytes}`)
},
})
```
## ➡️ Advanced Usage
### ✔️ Error as value
While the Fetch API does not throw an error when the response is not ok, _upfetch_ throws a `ResponseError` instead.
If you'd rather handle errors as values, set `reject` to return `false`. \
This allows you to customize the `parseResponse` function to return both successful data and error responses in a structured format.
```ts
const upfetch = up(fetch, () => ({
reject: () => false,
parseResponse: async (response) => {
const json = await response.json()
return response.ok
? { data: json, error: null }
: { data: null, error: json }
},
}))
```
Usage:
```ts
const { data, error } = await upfetch('/users/1')
```
### ✔️ Custom response parsing
By default _upfetch_ is able to parse