You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

up-fetch

Package Overview
Dependencies
Maintainers
1
Versions
138
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

up-fetch

Advanced fetch client builder for typescript.

latest
Source
npmnpm
Version
2.6.0
Version published
Maintainers
1
Created
Source

upfetch - advanced fetch client builder


upfetch


npm version npm bundle size license commit activity downloads per month


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 翻译)

Table of Contents

➡️ Highlights

  • 🚀 Lightweight - 1.6kB gzipped, no dependency
  • 🔒 Typesafe - Validate API responses with zod, valibot or 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

➡️ 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,
}))

✔️ Retry

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 config
await 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
      },
   },
}))

✔️ 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.

👉 ResponseValidationError

Raised when schema validation fails.
Use isResponseValidationError to identify this error type.

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:

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 },
})

✔️ 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') },
})

✔️ 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 })
   },
}))

✔️ 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:

const decoder = new TextDecoder()

upfetch('/ai-chatbot', {
   onResponseStreaming: ({ chunk }) => {
      const text = decoder.decode(chunk, { stream: true })
      console.log(text)
   },
})

✔️ Progress

👉 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 = 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.

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 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, 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/',
   // 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,
}))

➡️ API Reference

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>,
): UpFetch
OptionSignatureDescription
baseUrlstringBase URL for all requests.
onError(error, request) => voidExecutes on error.
onSuccess(data, request) => voidExecutes when the request successfully completes.
onRequest(request) => voidExecutes before the request is made.
onRequestStreaming(event, request) => voidExecutes each time a request chunk is send.
onResponseStreaming(event, response) => voidExecutes each time a response chunk is received.
onResponse(response, request) => voidExecutes once all retries are completed.
onRetry(ctx) => voidExecutes before each retry.
paramsobjectThe default query parameters.
parseResponse(response, request) => dataThe default success response parser.
If omitted json and text response are parsed automatically.
parseRejected(response, request) => errorThe default error response parser.
If omitted json and text response are parsed automatically
reject(response) => booleanDecide when to reject the response.
retryRetryOptionsThe default retry options.
serializeBody(body) => BodyInitThe default body serializer.
Restrict the valid body type by typing its first argument.
serializeParams(params) => stringThe default query parameter serializer.
timeoutnumberThe default timeout in milliseconds.
...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:

OptionSignatureDescription
baseUrlstringBase URL for the request.
onError(error, request) => voidExecutes on error.
onSuccess(data, request) => voidExecutes when the request successfully completes.
onRequest(request) => voidExecutes before the request is made.
onRequestStreaming(event, request) => voidExecutes each time a request chunk is send.
onResponseStreaming(event, response) => voidExecutes each time a response chunk is received.
onResponse(response, request) => voidExecutes once all retries are completed.
onRetry(ctx) => voidExecutes before each retry.
paramsobjectThe query parameters.
parseResponse(response, request) => dataThe success response parser.
parseRejected(response, request) => errorThe error response parser.
reject(response) => booleanDecide when to reject the response.
retryRetryOptionsThe retry options.
schemaStandardSchemaV1The schema to validate the response against.
The schema must follow the Standard Schema Specification.
serializeBody(body) => BodyInitThe body serializer.
Restrict the valid body type by typing its first argument.
serializeParams(params) => stringThe query parameter serializer.
timeoutnumberThe timeout in milliseconds.
...and all other fetch options

RetryOptions

OptionSignatureDescription
when(ctx) => booleanFunction that determines if a retry should happen based on the response or error
attemptsnumber | functionNumber of retry attempts or function to determine attempts based on request.
delaynumber | functionDelay between retries in milliseconds or function to determine delay based on attempt number

isResponseError(error)

Checks if the error is a ResponseError.

isResponseValidationError(error)

Checks if the error is a ResponseValidationError.

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


Share on:

s Share on Twitter



Keywords

fetch

FAQs

Package last updated on 09 Mar 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts