JSPM

  • Created
  • Published
  • Downloads 16319
  • Score
    100M100P100Q119247F
  • License ISC

Slim and flexible HTTP client based on the Fetch API.

Package Exports

  • up-fetch
  • up-fetch/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (up-fetch) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

up-fetch

Tiny fetch API wrapper with configurable defaults.

Highlights

  • Lightweight - 1kB gzipped, no dependency
  • Simple - same syntax as the fetch API with additional options and defaults
  • Intuitive - define the params and body as plain objects, the Response is parsed out of the box
  • Adaptive - bring your own serialization and parsing strategies for more complex cases
  • Reusable - create instances with custom defaults
  • Strongly typed - best in class type inferrence and autocomplete
  • Throws by default - when response.ok is false

Works in:
     ✅ All modern browsers
     ✅ Bun
     ✅ Node 18+
     ✅ Deno (with the npm: specifier)

QuickStart

npm i up-fetch

Create a new upfetch instance

import { up } from 'up-fetch' 

const upfetch = up(fetch)

Make a fetch request

const todos = await upfetch('https://a.b.c', {
   method: 'POST', 
   body: { hello: 'world' },
})

Since the upfetch options extend the fetch api options, anything that can be done with fetch can also be done with upfetch.

Features

Set defaults for an upfetch instance

up-fetch default behaviour can be entirely customized

const upfetch = up(fetch, () => ({
   baseUrl: 'https://a.b.c', 
   headers: { 'X-Header': 'hello world' }
}))

See the full options list for more details.

Set the url params as object

// before
fetch(`https://my.url/todos?search=${search}&skip=${skip}&take=${take}`)

// after
upfetch('https://my.url/todos', {
   params: { search, skip, take },
})

baseUrl option

Set the baseUrl when you create the instance

export const upfetch = up(fetch, () => ({
   baseUrl: 'https://my.url'
}))

You can then omit it on all requests

const todos = await upfetch('/todos')

Automatic Response parsing

The parsing method is customizable via the parseResponse option

// before
const response = await fetch('https://my.url/todos')
const todos = await response.json()

// after
const todos = await upfetch('https://my.url/todos')

throws by default

Throws a ResponseError when response.ok is false

A parsed error body is available with error.data.
The raw Response can be accessed with error.response.
The options used make the api call are available with error.options.

import { isResponseError } from 'up-fetch'
import { upfetch } from '...'

try {
   await upfetch('https://my.url/todos')
} 
catch(error){
   if(isResponseError(error)){
      console.log(error.data)
      console.log(error.response.status)
   } else {
      console.log('Unexpected error')
   }
}

Set the body as object

The 'Content-Type': 'application/json' header is automatically set when the body is a Jsonifiable object or array. Plain objects, arrays and classes with a toJSON method are Jsonifiable.

// before
fetch('https://my.url/todos', {
   method: 'POST',
   headers: {'Content-Type': 'application/json'},
   body: JSON.stringify({ post: 'Hello World'})
})

// after
upfetch('https://my.url/todos', {
   method: 'POST',
   body: { post: 'Hello World'}
})

Examples

Authentication

Since the options are evaluated at request time, the Authentication header can be defined when creating the instance

import { up } from 'up-fetch' 

const upfetch = up(fetch, () => {
   const token = localStorage.getItem('token')
   return {
      headers: { Authentication: token ? `Bearer ${token}` : undefined }
   }
})

localStorage.setItem('token', 'abcdef123456')
upfetch('/profile') // Authenticated request

localStorage.removeItem('token')
upfetch('/profile') // Non authenticated request

The same approach can be used with cookies instead of localStorage

Error handling

Two types of error can occur:

  1. a Response error when the server responds with an error code (response.ok is false)
  2. an Unexpected error produced when the server did not respond (eg. failed to fetch) or by the user code

By default response errors throw a ResponseError

up-fetch provides a type guard to check if the error is a ResponseError

import { upfetch } from '...'
import { isResponseError } from 'up-fetch'

// with try/catch
try {
   return await upfetch('https://a.b.c')
}
catch(error){
   if(isResponseError(error)) {
      // The server responded, the parsed data is available on the ReponseError
      console.log(error.data) 
   }
   else {
      console.log(error.message)
   }
}

// with Promise.catch
upfetch('https://a.b.c')
   .catch((error) => {
      if(isResponseError(error)) {
         // The server responded, the parsed data is available on the ReponseError
         console.log(error.data) 
      }
      else {
         console.log(error.message)
      }
   })

up-fetch also exports some listeners, useful for logging

import { up } from 'up-fetch' 
import { log } from './my-logging-service'

const upfetch = up(fetch, () => ({
   onResponseError(error){
      // error is of type ResponseError
      log.responseError(error)
   },
   onUnexpectedError(error){
      log.unexpectedError(error)
   },
   onError(error){
      // the error can be a ResponseError, or an unexpected Error
      log.error(error)
   },
}))

upfetch('/fail-to-fetch')
Delete a default option

Simply pass undefined

import { up } from 'up-fetch' 

const upfetch = up(fetch, () => ({
   cache: 'no-store',
   params: { expand: true, count: 1 },
   headers: { Authorization: `Bearer ${token}` }
}))

upfetch('https://a.b.c', {
   cache: undefined, // remove cache
   params: { expand: undefined }, // only remove `expand` from the params
   headers: undefined // remove all headers
})
Override a default option conditionally

You may sometimes need to conditionally override the default options provided in up. Javascript makes it a bit tricky:

import { up } from 'up-fetch' 

const upfetch = up(fetch, () => ({
   headers: { 'X-Header': 'value' }
}))

❌ Don't
// if `condition` is false, the header will be deleted
upfetch('https://a.b.c', {
   headers: { 'X-Header': condition ? 'newValue' : undefined }
})

In order to solve this problem, upfetch exposes the upOptions when the options (2nd arg) are defined as a function.
upOptions are stricly typed (const generic)

✅ Do
upfetch('https://a.b.c', (upOptions) => ({
   headers: { 'X-Header': condition ? 'newValue' : upOptions.headers['X-Header'] }
}))
Next.js App Router

Since up-fetch extends the fetch API, Next.js specific fetch options also work with up-fetch.

Choose a default caching strategy

import { up } from 'up-fetch' 

const upfetch = up(fetch, () => ({
   next: { revalidate: false }
}))

Override it for a specific request

upfetch('/posts', {
   next: { revalidate: 60 }
})

Types

See the type definitions file for more details

API

All options can be set either on up or on an upfetch instance except for the body

// set defaults for the instance
const upfetch = up(fetch, () => ({
   baseUrl: 'https://my.url.com',
   cache: 'no-store',
   headers: { 'Authorization': `Bearer ${token}` }
}))

// override the defaults for a specific call
upfetch('todos', {
   baseUrl: 'https://another.url.com',
   cache: 'force-cache'
})

upfetch adds the following options to the fetch API.

<baseUrl>

Type: string

Sets the base url for the requests

Example:

const upfetch = up(fetch, () => ({ 
   baseUrl: 'https://example.com' 
}))

// make a GET request to 'https://example.com/id'
upfetch('/id')

// change the baseUrl for a single request
upfetch('/id', { baseUrl: 'https://another-url.com' })

<params>

Type: { [key: string]: any }

The url search params.
The default params defined in up and the upfetch instance params are shallowly merged.
Only non-nested objects are supported by default. See the serializeParams option for nested objects.

Example:

const upfetch = up(fetch, () => ({ 
   params : { expand: true  }
}))

// `expand` can be omitted
// the request is sent to: https://example.com/?expand=true&page=2&limit=10
upfetch('https://example.com', { 
   params: { page: 2, limit: 10 }
})

// override the `expand` value
// https://example.com/?expand=false&page=2&limit=10
upfetch('https://example.com', { 
   params: { page: 2, limit: 10, expand: false }
})

// remove `expand` from the params
// https://example.com/?expand=false&page=2&limit=10
upfetch('https://example.com', { 
   params: { page: 2, limit: 10, expand: undefined }
})

<body>

Type: BodyInit | JsonifiableObject | JsonifiableArray | null

Note that this option is not available on up

The body of the request.
Can be pretty much anything.
See the serializeBody for more details.

Example:

upfetch('/todos', { 
   method: 'POST',
   body: { hello: 'world' } 
})

<serializeParams>

Type: (params: { [key: string]: any } ) => string

Customize the params serialization into a query string.
The default implementation only supports non-nested objects.

Example:

import qs from 'qs'

// add support for nested objects using the 'qs' library
const upfetch = up(fetch, () => ({
   serializeParams: (params) => qs.stringify(params)
}))

// https://example.com/todos?a[b]=c
upfetch('https://example.com/todos', { 
   params: { a: { b: 'c' } }
})

<serializeBody>

Type: (body: JsonifiableObject | JsonifiableArray) => string

Default: JSON.stringify

Customize the body serialization into a string.
The body is passed to serializeBody when it is a plain object, an array or a class instance with a toJSON method. The other body types remain untouched

Example:

import stringify from 'json-stringify-safe'

// Add support for circular references.
const upfetch = up(fetch, () => ({
   serializeBody: (body) => stringify(body)
}))

upfetch('https://example.com/', { 
   body: { now: 'imagine a circular ref' }
})

<parseResponse>

Type: ParseResponse<TData> = (response: Response, options: ComputedOptions) => Promise<TData>

Customize the fetch response parsing.
By default json and text responses are parsed

Example:

// parse a blob
const fetchBlob = up(fetch, () => ({
   parseResponse: (res) => res.blob()
}))

fetchBlob('https://example.com/')

// disable the default parsing
const upfetch = up(fetch, () => ({
   parseResponse: (res) => res
}))

const response = await upfetch('https://example.com/')
const data = await response.json()

<parseResponseError>

Type: ParseResponseError<TError> = (response: Response, options: ComputedOptions) => Promise<TError>

Customize the parsing of a fetch response error (when response.ok is false)
By default a ResponseError is created

Example:

const upfetch = up(fetch, () => ({
   parseResponseError: (res) => new CustomResponseError(res)
}))

// using the onResponseError callback
upfetch('https://example.com/', {
   onResponseError(error){
      // the error is already typed
   }
})

// using try/catch
try {
   await upfetch('https://example.com/')
}
catch(error){
   if(error instanceof CustomResponseError){
      // handle the error
   }
   else {
      // Unexpected error
   }
}

<parseUnexpectedError>

Type: ParseUnexpectedError<TError> = (error: Error, options: ComputedOptions) => TError

Modify unexpected errors. Unexpected errors are generated when:

  • the server did not respond or could not be reached (eg. failed to fetch)
  • the user code produced an error

Example:

// extract the error.message for all unexpected errors
const upfetch = up(fetch, () => ({
   parseUnexpectedError: (error) => error.message
}))

// using the onUnknwonError callback
upfetch('https://example.com/', {
   onUnexpectedError(error){
      // error is of type string
   }
})

// using try/catch
try {
   await upfetch('https://example.com/')
}
catch(error){
   if(isResponseError(error)){
      // response error
   }
   else {
      // unexpected error 
   }
}

<onBeforeFetch>

Type: (options: ComputedOptions) => void

Called before the fetch call is made.

Example:

const upfetch = up(fetch, () => ({
   onBeforeFetch: (options) => console.log('first')
}))

upfetch('https://example.com/', {
   onBeforeFetch: (options) => console.log('second')
})

<onSuccess>

Type: <TData>(data: TData, options: ComputedOptions) => void

Called when everything went fine

Example:

const upfetch = up(fetch, () => ({
   onSuccess: (data, options) => console.log('first')
}))

upfetch('https://example.com/', {
   onSuccess: (data, options) => console.log('second')
})

<onResponseError>

Type: <TResponseError>(error: TResponseError, options: ComputedOptions) => void

Called when a response error was thrown (response.ok is false).
Called before onError

Example:

const upfetch = up(fetch, () => ({
   onResponseError: (error, options) => console.log('first')
}))

upfetch('https://example.com/', {
   onResponseError: (error, options) => console.log('second')
})

<onUnexpectedError>

Type: <TUnexpectedError>(error: TUnexpectedError, options: ComputedOptions) => void

Called when a unexpected error was thrown (an error that is not a response error). \ Called before onError

Example:

const upfetch = up(fetch, () => ({
   onUnexpectedError: (error, options) => console.log('first')
}))

upfetch('https://example.com/', {
   onUnexpectedError: (error, options) => console.log('second')
})

<onError>

Type: <TError>(error: TError, options: ComputedOptions) => void

Called when an error was thrown (either a response or an unexpected error). \ Called after onResponseError and onUnexpectedError

Example:

const upfetch = up(fetch, () => ({
   onError: (error, options) => console.log('first')
}))

upfetch('https://example.com/', {
   onError: (error, options) => console.log('second')
})
const upfetch = up(fetch, () => ({
   onResponseError: (error, options) => console.log('first')
   onError: (error, options) => console.log('third')
}))

upfetch('https://example.com/', {
   onResponseError: (error, options) => console.log('second')
   onError: (error, options) => console.log('fourth')
})