Advanced fetch client builder for typescript.
npm install up-fetch_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.
- Highlights
- QuickStart
- Key Features
- Request Configuration
- Simple Query Parameters
- Automatic Body Handling
- Schema Validation
- Lifecycle Hooks
- Timeout
- Retry
- Error Handling
- Usage
- Authentication
- Delete a default option
- FormData
- Multiple fetch clients
- Streaming
- Progress
- Advanced Usage
- Error as value
- Custom response parsing
- Custom response errors
- Custom params serialization
- Custom body serialization
- Defaults based on the request
- API Reference
- Feature Comparison
- Environment Support
- 🚀 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
``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.
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.
👎 With raw fetch:
`tshttps://api.example.com/todos?search=${search}&skip=${skip}&take=${take}
fetch(
,`
)
👍 With _upfetch_:
`ts`
upfetch('/todos', {
params: { search, skip, take },
})
Use the [serializeParams][api-reference] option to customize the query parameter serialization.
👎 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.
Check out the [serializeBody][api-reference] option to customize the body serialization.
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(),
}),
})
`
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
},
}))
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,
}))
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
},
},
}))
#### 👉 ResponseError
Raised when response.ok is false. \isResponseError
Use 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.
#### 👉 ValidationError
Raised when schema validation fails. \
Use isValidationError to identify this error type.
`ts
import { isValidationError } from 'up-fetch'
try {
await upfetch('/todos/1', { schema: todoSchema })
} catch (error) {
if (isValidationError(error)) {
console.log(error.issues)
}
}
`
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() },
}))
Simply pass undefined:
`ts`
upfetch('/todos', {
signal: undefined,
})
Also works for single params and headers:
`ts`
upfetch('/todos', {
headers: { Authorization: undefined },
})
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') },
})
`
You can create multiple upfetch instances with different defaults:
`tsBearer ${process.env.API_KEY}
const fetchMovie = up(fetch, () => ({
baseUrl: 'https://api.themoviedb.org',
headers: {
accept: 'application/json',
Authorization: ,
},
}))
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 })
},
}))
`
_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 streamedtransferredBytes: number
- : The amount of data transferred so fartotalBytes?: 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)
},
})
`
#### 👉 Upload progress:
`tsProgress: ${transferredBytes} / ${totalBytes}
upfetch('/upload', {
method: 'POST',
body: new File(['large file'], 'foo.txt'),
onRequestStreaming: ({ transferredBytes, totalBytes }) => {
console.log()`
},
})
#### 👉 Download progress:
`tsProgress: ${transferredBytes} / ${totalBytes}
upfetch('/download', {
onResponseStreaming: ({
transferredBytes,
totalBytes = transferredBytes,
}) => {
console.log()`
},
})
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. \parseResponse
This allows you to customize the 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')
By default _upfetch_ is able to parse json and text sucessful responses automatically.
The parseResponse method is called when reject returns false.
You can use that option to parse other response types.
`ts`
const upfetch = up(fetch, () => ({
parseResponse: (response) => response.blob(),
}))
💡 Note that the parseResponse method is called only when reject returns false.
By default _upfetch_ throws a ResponseError when reject returns true.
If you want to throw a custom error or customize the error message, you can pass a function to the parseRejected option.
`tsRequest failed with status ${status}: ${JSON.stringify(data)}
const upfetch = up(fetch, () => ({
parseRejected: async (response) => {
const data = await response.json()
const status = response.status
// custom error message
const message = `
// you can return a custom error class as well
return new ResponseError({ message, data, request, response })
},
}))
By default _upfetch_ serializes the params using URLSearchParams.
You can customize the params serialization by passing a function to the serializeParams option.
`ts
import queryString from 'query-string'
const upfetch = up(fetch, () => ({
serializeParams: (params) => queryString.stringify(params),
}))
`
By default _upfetch_ serializes the plain objects using JSON.stringify.
You can customize the body serialization by passing a function to the serializeBody option. It lets you:
- restrict the valid body type by typing its first argument
- transform the body in a valid BodyInit type
The following example show how to restrict the valid body type to Record and serialize it using JSON.stringify:
`ts
// Restrict the body type to Record
const upfetch = up(fetch, () => ({
serializeBody: (body: Record
}))
// ❌ type error: the body is not a Record
upfetch('https://a.b.c/todos', {
method: 'POST',
body: [['title', 'New Todo']],
})
// ✅ works fine with Record
upfetch('https://a.b.c/todos', {
method: 'POST',
body: { title: 'New Todo' },
})
`
The following example uses superjson to serialize the body. The valid body type is inferred from SuperJSON.stringify.
`ts
import SuperJSON from 'superjson'
const upfetch = up(fetch, () => ({
serializeBody: SuperJSON.stringify,
}))
`
The default options receive the fetcher arguments, this allows you to tailor the defaults based on the actual request.
`tsBearer ${getToken()}
const upfetch = up(fetch, (input, options) => ({
baseUrl: 'https://example.com/',
// Add authentication only for protected routes
headers: {
Authorization:
typeof input === 'string' && input.startsWith('/api/protected/')
? `
: undefined,
},
// Add tracking params only for public endpoints
params: {
trackingId:
typeof input === 'string' && input.startsWith('/public/')
? crypto.randomUUID()
: undefined,
},
// Increase timeout for long-running operations
timeout:
typeof input === 'string' && input.startsWith('/export/') ? 30000 : 5000,
}))
Creates a new upfetch instance with optional default options.
`ts`
function up(
fetchFn: typeof globalThis.fetch,
getDefaultOptions?: (
input: RequestInit,
options: FetcherOptions,
) => DefaultOptions | Promise
): UpFetch
| Option | Signature | Description |
| -------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------- |
| baseUrl | string | Base URL for all requests. |onError
| | (error, request) => void | Executes on error. |onSuccess
| | (data, request) => void | Executes when the request successfully completes. |onRequest
| | (request) => void | Executes before the request is made. |onRequestStreaming
| | (event, request) => void | Executes each time a request chunk is send. |onResponseStreaming
| | (event, response) => void | Executes each time a response chunk is received. |onResponse
| | (response, request) => void | Executes once all retries are completed. |onRetry
| | (ctx) => void | Executes before each retry. |params
| | object | The default query parameters. |parseResponse
| | (response, request) => data | The default success response parser. json
If omitted and text response are parsed automatically. |parseRejected
| | (response, request) => error | The default error response parser. json
If omitted and text response are parsed automatically |reject
| | (response) => boolean | Decide when to reject the response. |retry
| | RetryOptions | The default retry options. |serializeBody
| | (body) => BodyInit | The default body serializer.body
Restrict the valid type by typing its first argument. |serializeParams
| | (params) => string | The default query parameter serializer. |timeout
| | number | The default timeout in milliseconds. |
| _...and all other fetch options_ | | |
Makes a fetch request with the given options.
`ts`
function upfetch(
url: string | URL | Request,
options?: FetcherOptions,
): Promise
Options:
| Option | Signature | Description |
| -------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| baseUrl | string | Base URL for the request. |onError
| | (error, request) => void | Executes on error. |onSuccess
| | (data, request) => void | Executes when the request successfully completes. |onRequest
| | (request) => void | Executes before the request is made. |onRequestStreaming
| | (event, request) => void | Executes each time a request chunk is send. |onResponseStreaming
| | (event, response) => void | Executes each time a response chunk is received. |onResponse
| | (response, request) => void | Executes once all retries are completed. |onRetry
| | (ctx) => void | Executes before each retry. |params
| | object | The query parameters. |parseResponse
| | (response, request) => data | The success response parser. |parseRejected
| | (response, request) => error | The error response parser. |reject
| | (response) => boolean | Decide when to reject the response. |retry
| | RetryOptions | The retry options. |schema
| | StandardSchemaV1 | The schema to validate the response against.serializeBody
The schema must follow the [Standard Schema Specification][standard-schema]. |
| | (body) => BodyInit | The body serializer.body
Restrict the valid type by typing its first argument. |serializeParams
| | (params) => string | The query parameter serializer. |timeout
| | number | The timeout in milliseconds. |
| _...and all other fetch options_ | | |
| Option | Signature | Description |
| ---------- | -------------------- | -------------------------------------------------------------------------------------------- |
| when | (ctx) => boolean | Function that determines if a retry should happen based on the response or error |attempts
| | number \| function | Number of retry attempts or function to determine attempts based on request. |delay
| | number \| function | Delay between retries in milliseconds or function to determine delay based on attempt number |
Checks if the error is a ResponseError.
Checks if the error is a ValidationError.
Determines whether a value can be safely converted to json.
Are considered jsonifiable:
- plain objects
- arrays
- class instances with a toJSON` method
Check out the [Feature Comparison][comparison] table to see how _upfetch_ compares to other fetching libraries.
- ✅ Browsers (Chrome, Firefox, Safari, Edge)
- ✅ Node.js (18.0+)
- ✅ Bun
- ✅ Deno
- ✅ Cloudflare Workers
- ✅ Vercel Edge Runtime
[![s][bsky-badge]][bsky-link]
[![Share on Twitter][tweet-badge]][tweet-link]
[bsky-badge]: https://img.shields.io/badge/Bluesky-0085ff?logo=bluesky&logoColor=fff
[bsky-link]: https://bsky.app/intent/compose?text=https%3A%2F%2Fgithub.com%2FL-Blondy%2Fup-fetch
[tweet-badge]: https://img.shields.io/badge/Twitter-0f1419?logo=x&logoColor=fff
[tweet-link]: https://twitter.com/intent/tweet?text=https%3A%2F%2Fgithub.com%2FL-Blondy%2Fup-fetch
[zod]: https://zod.dev/
[valibot]: https://valibot.dev/
[arktype]: https://arktype.dev/
[standard-schema]: https://github.com/standard-schema/standard-schema
[standard-schema-libs]: https://github.com/standard-schema/standard-schema?tab=readme-ov-file#what-schema-libraries-implement-the-spec
[api-reference]: #️-api-reference
[comparison]: https://github.com/L-Blondy/up-fetch/blob/master/COMPARISON.md