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.
🚀 Try v2 Beta!
Version 2 of upfetch is now available in beta! The changes mainly impact advanced use cases, so most projects won’t require any modifications. Give it a try with:
npm i up-fetch@2.0.0-beta.1
Check out the Migration Guide for details about changes and how to upgrade.
For a complete overview of new features, see the v2 documentation.
Table of Contents
- Highlights
- QuickStart
- Key Features
- Usage
- Advanced Usage
- API Reference
- Feature Comparison
- Environment Support
➡️ Highlights
- 🚀 Lightweight - 1.2kB gzipped, no dependency
- 🔒 Typesafe - Validate API responses with zod, valibot or arktype
- 🛠️ Practical API - Use objects for
params
andbody
, get parsed responses automatically - 🎨 Flexible Config - Set defaults like
baseUrl
orheaders
once, use everywhere - 🤝 Familiar - same API as fetch with additional options and sensible defaults
➡️ QuickStart
npm i up-fetch
Create a new upfetch instance:
import { up } from 'up-fetch'
export const upfetch = up(fetch)
Make a fetch request with schema validation:
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:
const upfetch = up(fetch, () => ({
baseUrl: 'https://a.b.c',
timeout: 30000,
}))
Check out the the API Reference for the full list of options.
✔️ Simple Query Parameters
👎 With raw fetch:
fetch(
`https://api.example.com/todos?search=${search}&skip=${skip}&take=${take}`,
)
👍 With upfetch:
upfetch('/todos', {
params: { search, skip, take },
})
Use the serializeParams option to customize the query parameter serialization.
✔️ Automatic Body Handling
👎 With raw fetch:
fetch('https://api.example.com/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'New Todo' }),
})
👍 With upfetch:
upfetch('/todos', {
method: 'POST',
body: { title: 'New Todo' },
})
upfetch also supports all fetch body types.
Check out the serializeBody option to customize the body serialization.
✔️ Schema Validation
Since upfetch follows the Standard Schema Specification it can be used with any schema library that implements the spec.
See the full list here.
👉 With zod 3.24+
import { z } from 'zod'
const posts = await upfetch('/posts/1', {
schema: z.object({
id: z.number(),
title: z.string(),
}),
})
👉 With valibot 1.0+
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:
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:
upfetch('/todos', {
timeout: 3000,
})
Set a default timeout for all requests:
const upfetch = up(fetch, () => ({
timeout: 5000,
}))
✔️ Error Handling
👉 ResponseError
Raised when response.ok
is false
.
Use isResponseError
to identify this error type.
import { isResponseError } from 'up-fetch'
try {
await upfetch('/todos/1')
} catch (error) {
if (isResponseError(error)) {
console.log(error.status)
}
}
- Use the parseRejected option to throw a custom error instead.
- Use the reject option to decide when to throw.
👉 ValidationError
Raised when schema validation fails.
Use isValidationError
to identify this error type.
import { isValidationError } from 'up-fetch'
try {
await upfetch('/todos/1', { schema: todoSchema })
} catch (error) {
if (isValidationError(error)) {
console.log(error.issues)
}
}
➡️ Usage
✔️ Authentication
You can easily add authentication to all requests by setting a default header:
const upfetch = up(fetch, () => ({
headers: { Authorization: localStorage.getItem('bearer-token') },
}))
The bearer token will be retrieved from localStorage
before each request.
✔️ Delete a default option
Simply pass undefined
:
upfetch('/todos', {
signal: undefined,
})
✔️ FormData
Grab the FormData from a form
.
const form = document.querySelector('#my-form')
upfetch('/todos', {
method: 'POST',
body: new FormData(form),
})
Or create FormData from an object:
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') },
})
✔️ HTTP Agent
Since upfetch is "fetch agnostic", you can use undici instead of the native fetch implementation.
On a single request:
import { fetch, Agent } from 'undici'
const upfetch = up(fetch)
const data = await upfetch('https://a.b.c', {
dispatcher: new Agent({
keepAliveTimeout: 10,
keepAliveMaxTimeout: 10,
}),
})
On all requests:
import { fetch, Agent } from 'undici'
const upfetch = up(fetch, () => ({
dispatcher: new Agent({
keepAliveTimeout: 10,
keepAliveMaxTimeout: 10,
}),
}))
✔️ Multiple fetch clients
You can create multiple upfetch instances with different defaults:
const fetchJson = up(fetch)
const fetchBlob = up(fetch, () => ({
parseResponse: (res) => res.blob(),
}))
const fetchText = up(fetch, () => ({
parseResponse: (res) => res.text(),
}))
➡️ 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.
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:
const { data, error } = await upfetch('/users/1')
✔️ Custom response parsing
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.
const upfetch = up(fetch, () => ({
parseResponse: (response) => response.blob(),
}))
💡 Note that the parseResponse
method is called only when reject
returns false
.
✔️ Custom response errors
By default upfetch throws a ResponseError
when reject
returns true
.
If you want to throw a custom error instead, you can pass a function to the parseRejected
option.
const upfetch = up(fetch, () => ({
parseRejected: async (response) => {
const status = response.status
const data = await response.json()
return new CustomError(status, data)
},
}))
✔️ Custom params serialization
By default upfetch serializes the params using URLSearchParams
.
You can customize the params serialization by passing a function to the serializeParams
option.
import queryString from 'query-string'
const upfetch = up(fetch, () => ({
serializeParams: (params) => queryString.stringify(params),
}))
✔️ Custom body serialization
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<string, any>
and serialize it using JSON.stringify
:
// Restrict the body type to Record<string, any> and serialize it
const upfetch = up(fetch, () => ({
serializeBody: (body: Record<string, any>) => JSON.stringify(body),
}))
// ❌ type error: the body is not a Record<string, any>
upfetch('https://a.b.c/todos', {
method: 'POST',
body: [['title', 'New Todo']],
})
// ✅ works fine with Record<string, any>
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
.
import SuperJSON from 'superjson'
const upfetch = up(fetch, () => ({
serializeBody: SuperJSON.stringify,
}))
✔️ Defaults based on the request
The default options receive the fetcher arguments, this allows you to tailor the defaults based on the actual request.
const upfetch = up(fetch, (input, options) => ({
baseUrl: 'https://example.com/',
headers: {
// Add authentication only for protected routes
Authorization:
typeof input === 'string' && input.startsWith('/api/protected/')
? `Bearer ${getToken()}`
: 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,
}))
➡️ API Reference
up(fetch, getDefaultOptions?)
Creates a new upfetch instance with optional default options.
function up(
fetchFn: typeof globalThis.fetch,
getDefaultOptions?: (fetcherOptions: FetcherOptions) => DefaultOptions,
): UpFetch
Option | Signature | Description |
---|---|---|
baseUrl |
string |
Base URL for all requests. |
params |
object |
The default query parameters. |
onRequest |
(options) => void |
Executes before the request is made. |
onError |
(error, options) => void |
Executes on error. |
onSuccess |
(data, options) => void |
Executes when the request successfully completes. |
parseResponse |
(response, options) => data |
The default success response parser. If omitted json and text response are parsed automatically. |
parseRejected |
(response, options) => error |
The default error response parser. If omitted json and text response are parsed automatically |
serializeBody |
(body) => BodyInit |
The default body serializer. Restrict the valid body type by typing its first argument. |
serializeParams |
(params) => string |
The default query parameter serializer. |
timeout |
number |
The default timeout in milliseconds. |
reject |
(response) => boolean |
Decide when to reject the response. |
...and all other fetch options |
upfetch(url, options?)
Makes a fetch request with the given options.
function upfetch(
url: string | URL | Request,
options?: FetcherOptions,
): Promise<any>
Options:
Option | Signature | Description |
---|---|---|
baseUrl |
string |
Base URL for the request. |
params |
object |
The query parameters. |
parseResponse |
(response, options) => data |
The success response parser. |
parseRejected |
(response, options) => error |
The error response parser. |
schema |
StandardSchemaV1 |
The schema to validate the response against. The schema must follow the Standard Schema Specification. |
serializeBody |
(body) => BodyInit |
The body serializer. Restrict the valid body type by typing its first argument. |
serializeParams |
(params) => string |
The query parameter serializer. |
timeout |
number |
The timeout in milliseconds. |
reject |
(response) => boolean |
Decide when to reject the response. |
...and all other fetch options |
isResponseError(error)
Checks if the error is a ResponseError
.
isValidationError(error)
Checks if the error is a ValidationError
.
isJsonifiable(value)
Determines whether a value can be safely converted to json
.
Are considered jsonifiable:
- plain objects
- arrays
- class instances with a
toJSON
method
➡️ Feature Comparison
Check out the Feature Comparison table to see how upfetch compares to other fetching libraries.
➡️ Environment Support
- ✅ Browsers (Chrome, Firefox, Safari, Edge)
- ✅ Node.js (18.0+)
- ✅ Bun
- ✅ Deno
- ✅ Cloudflare Workers
- ✅ Vercel Edge Runtime