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.
params
and body
, get parsed responses automaticallybaseUrl
or headers
once, use everywhereCreate 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.
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.
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(), }), })
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 }, }))
Set a timeout for one request:
upfetch('/todos', { timeout: 3000, })
Set a default timeout for all requests:
const upfetch = up(fetch, () => ({ timeout: 5000, }))
The retry functionality allows you to automatically retry failed requests with configurable attempts, delay, and condition.
const upfetch = up(fetch, () => ({ retry: { attempts: 3, delay: 1000, }, }))
Examples:
Per-request retry configawait upfetch('/api/data', { method: 'DELETE', retry: { attempts: 2, }, })Exponential retry delay
const upfetch = up(fetch, () => ({ retry: { attempts: 3, delay: (ctx) => ctx.attempt ** 2 * 1000, }, }))Retry based on the request method
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
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
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 }, }, }))
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) } }
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) } }
You can easily add authentication to all requests by setting a default header.
Retrieve the token from localStorage
before each request:
const upfetch = up(fetch, () => ({ headers: { Authorization: localStorage.getItem('bearer-token') }, }))
Retrieve an async token:
const upfetch = up(fetch, async () => ({ headers: { Authorization: await getToken() }, }))✔️ Delete a default option
Simply pass undefined
:
upfetch('/todos', { signal: undefined, })
Also works for single params
and headers
:
upfetch('/todos', { headers: { Authorization: undefined }, })
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') }, })✔️ Multiple fetch clients
You can create multiple upfetch instances with different defaults:
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 }) }, }))
upfetch provides powerful streaming capabilities through onRequestStreaming
for upload operations, and onResponseStreaming
for download operations.
Both handlers receive the following event object plus the request/response:
type StreamingEvent = { chunk: Uint8Array // The current chunk of data being streamed totalBytes: number // Total size of the data transferredBytes: number // Amount of data transferred so far }
The totalBytes
property of the event is read from the "Content-Length"
header.
For request streaming, if the header is not present, the total bytes are read from the request body.
Here's an example of processing a streamed response from an AI chatbot:
const decoder = new TextDecoder() upfetch('/ai-chatbot', { onResponseStreaming: (event, response) => { const text = decoder.decode(event.chunk, { stream: true }) console.log(text) }, })
Upload progress:
upfetch('/upload', { method: 'POST', body: new File(['large file'], 'foo.txt'), onRequestStreaming: ({ transferredBytes, totalBytes }) => { console.log(`Progress: ${transferredBytes} / ${totalBytes}`) }, })
Download progress:
upfetch('/download', { onResponseStreaming: ({ transferredBytes, totalBytes }) => { console.log(`Progress: ${transferredBytes} / ${totalBytes}`) }, })
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
.
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.
const upfetch = up(fetch, () => ({ parseRejected: async (response) => { const data = await response.json() const status = response.status // custom error message const message = `Request failed with status ${status}: ${JSON.stringify(data)}` // you can return a custom error class as well return new ResponseError({ message, data, request, response }) }, }))✔️ 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:
BodyInit
typeThe 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/', // Add authentication only for protected routes headers: { 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, }))up(fetch, getDefaultOptions?)
Creates a new upfetch instance with optional default options.
function up( fetchFn: typeof globalThis.fetch, getDefaultOptions?: ( input: RequestInit, options: FetcherOptions, ) => DefaultOptions | Promise<DefaultOptions>, ): UpFetchOption 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. onRetry
(ctx) => void
Executes before each retry. params
object
The default query parameters. parseResponse
(response, request) => data
The default success response parser.
json
and text
response are parsed automatically. parseRejected
(response, request) => error
The default error response parser.
json
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
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.
function upfetch( url: string | URL | Request, options?: FetcherOptions, ): Promise<any>
Options:
Option Signature DescriptionbaseUrl
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. 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
(body) => BodyInit
The body serializer.
body
type by typing its first argument. serializeParams
(params) => string
The query parameter serializer. timeout
number
The timeout in milliseconds. ...and all other fetch options
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:
toJSON
methodCheck out the Feature Comparison table to see how upfetch compares to other fetching libraries.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4