Skip to content

Latest commit

 

History

History
1116 lines (826 loc) · 28.8 KB

README.md

File metadata and controls

1116 lines (826 loc) · 28.8 KB

ya-fetch

Super light-weight wrapper around fetch

📦 Install

$ npm install --save ya-fetch

🌎 Import in a browser

<script type="module">
  import * as YF from 'https://esm.sh/ya-fetch'
</script>

For readable version import from https://esm.sh/ya-fetch/esm/index.js?raw.

🔗 Playground on CodePen.

⬇️ Jump to

👀 Examples

Import module

import * as YF from 'ya-fetch' // or from 'https://esm.sh/ya-fetch' in browsers

Create an instance

const api = YF.create({ resource: 'https://jsonplaceholder.typicode.com' })

Related

Send & receive JSON

await api.post('/posts', { json: { title: 'New Post' } }).json()
Same code with native fetch
fetch('http://example.com/posts', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    accept: 'application/json',
  },
  body: JSON.stringify({ title: 'New Post' }),
}).then((res) => {
  if (res.ok) {
    return res.json()
  }

  throw new Error('Request failed')
})

Related

Set search params

await api.get('/posts', { params: { userId: 1 } }).json()
Same code with native fetch
fetch('http://example.com/posts?id=1').then((res) => {
  if (res.ok) {
    return res.json()
  }

  throw new Error('Request failed')
})

Related

Set options dynamically

You can use an async or regular function to modify the options before the request.

import { getToken } from './global-state'

const authorized = YF.create({
  resource: 'https://jsonplaceholder.typicode.com',
  async onRequest(url, options) {
    options.headers.set('Authorization', `Bearer ${await getToken()}`)
  },
})

Related

Send form data (native fetch behaviour)

Provide FormData object inside body to send multipart/form-data request, headers are set automatically by following native fetch behaviour.

const body = new FormData()

body.set('title', 'My Title')
body.set('image', myFile, 'image.jpg')

// will send 'Content-type': 'multipart/form-data' request
await api.post('/posts', { body })

Related

Set timeout

Cancel request if it is not fulfilled in period of time.

try {
  await api.get('/posts', { timeout: 300 }).json()
} catch (error) {
  if (error instanceof YF.TimeoutError) {
    // do something, or nothing
  }
}
Same code with native fetch
const controller = new AbortController()

setTimeout(() => {
  controller.abort()
}, 300)

fetch('http://example.com/posts', {
  signal: controller.signal,
  headers: {
    accept: 'application/json',
  },
})
  .then((res) => {
    if (res.ok) {
      return res.json()
    }

    throw new Error('Request failed')
  })
  .catch((error) => {
    if (error.name === 'AbortError') {
      // do something
    }
  })

Related

Provide custom search params serializer

By default parsed and stringified with URLSearchParams and additional improvements to parsing of arrays.

import queryString from 'query-string'

const api = YF.create({
  resource: 'https://jsonplaceholder.typicode.com',
  serialize: (params) =>
    queryString.stringify(params, { arrayFormat: 'bracket' }),
})

// will send request to: 'https://jsonplaceholder.typicode.com/posts?userId=1&tags[]=1&tags[]=2'
await api.get('/posts', { params: { userId: 1, tags: [1, 2] } })

Related

Extend an instance

It's also possible to create extended version of existing by providing additional options. In this example the new instance will have https://jsonplaceholder.typicode.com/posts as resource inside the extended options:

const posts = api.extend({ resource: '/posts' })

await posts.get().json() // → [{ id: 0, title: 'Hello' }, ...]
await posts.get('/1').json() // → { id: 0, title: 'Hello' }
await posts.post({ json: { title: 'Bye' } }).json() // → { id: 1, title: 'Bye' }
await posts.patch('/0', { json: { title: 'Hey' } }).json() // → { id: 0, title: 'Hey' }
await posts.delete('/1').void() // → undefined

Related

Node.js Support

Install node-fetch and setup it as globally available variable.

npm install --save node-fetch
import fetch, { Headers, Request, Response, FormData } from 'node-fetch'

globalThis.fetch = fetch
globalThis.Headers = Headers
globalThis.Request = Request
globalThis.Response = Response
globalThis.FormData = FormData

⚠️ Please, note node-fetch v2 may hang on large response when using .clone() or response type shortcuts (like .json()) because of smaller buffer size (16 kB). Use v3 instead and override default value of 10mb when needed with highWaterMark option.

const instance = YF.create({
  highWaterMark: 1024 * 1024 * 10, // default
})

↕️ Jump to

📖 API

import * as YF from 'ya-fetch'

// YF.create
// YF.get
// YF.post
// YF.patch
// YF.put
// YF.delete
// YF.head

create

function create(options: Options): Instance

Creates an instance with preset default options. Specify parts of resource url, headers, response or error handlers, and more:

const instance = YF.create({
  resource: 'https://jsonplaceholder.typicode.com',
  headers: {
    'x-from': 'Website',
  },
})

// instance.get
// instance.post
// instance.patch
// instance.put
// instance.delete
// instance.head
// instance.extend

Related

Returns instance

interface Instance {
  get(resource?: string, options?: Options): ResponsePromise
  post(resource?: string, options?: Options): ResponsePromise
  patch(resource?: string, options?: Options): ResponsePromise
  put(resource?: string, options?: Options): ResponsePromise
  delete(resource?: string, options?: Options): ResponsePromise
  head(resource?: string, options?: Options): ResponsePromise
  extend(options?: Options): Instance
}

Instance with preset options, and extend method:

get
post
patch
put
delete
head

function requestMethod(resource?: string, options?: Options): ResponsePromise

Same as get, post, patch, put, delete, or head function exported from the module, but with preset options.

extend

function extend(options?: Options): Instance

Take an instance and extend it with additional options, the headers and params will be merged with values provided in parent instance, the resource will concatenated to the parent value.

const instance = YF.create({
  resource: 'https://jsonplaceholder.typicode.com',
  headers: { 'X-Custom-Header': 'Foo' },
})

// will have combined `resource` and merged `headers`
const extended = instance.extend({
  resource: '/posts'
  headers: { 'X-Something-Else': 'Bar' },
})

// will send request to: 'https://jsonplaceholder.typicode.com/posts/1'
await extended.post('/1', { json: { title: 'Hello' } })
Related

get
post
patch
put
delete
head

function requestMethod(resource?: string, options?: Options): ResponsePromise

Calls fetch with preset request method and options:

await YF.get('https://jsonplaceholder.typicode.com/posts').json()
// → [{ id: 0, title: 'Hello' }, ...]

The same functions are returned after creating an instance with preset options:

const instance = YF.create({ resource: 'https://jsonplaceholder.typicode.com' })
await instance.get('/posts').json()
// → [{ id: 0, title: 'Hello' }, ...]

Related

Returns response promise

interface ResponsePromise extends Promise<Response> {
  json<T>(): Promise<T>
  text(): Promise<string>
  blob(): Promise<Blob>
  arrayBuffer(): Promise<ArrayBuffer>
  formData(): Promise<FormData>
  void(): Promise<void>
}

ResponsePromise is a promise based object with exposed body methods:

json

function json<T>(): Promise<T>

Sets Accept: 'application/json' in headers and parses the body as JSON:

interface Post {
  id: number
  title: string
  content: string
}

const post = await instance.get('/posts').json<Post[]>()
Same code with native fetch
interface Post {
  id: number
  title: string
  content: string
}

const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
  headers: { Accept: 'application/json' },
})

if (response.ok) {
  const post: Post[] = await response.json()
}
Related

text

function text(): Promise<string>

Sets Accept: 'text/*' in headers and parses the body as plain text:

await instance.delete('/posts/1').text() // → 'OK'
Same code with native fetch
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
  headers: { Accept: 'text/*' },
  method: 'DELETE',
})

if (response.ok) {
  await response.text() // → 'OK'
}

formData

function formData(): Promise<FormData>

Sets Accept: 'multipart/form-data' in headers and parses the body as FormData:

const body = new FormData()

body.set('title', 'Hello world')
body.set('content', '🌎')

const data = await instance.post('/posts', { body }).formData()

data.get('id') // → 1
Same code with native fetch
const body = new FormData()

body.set('title', 'Hello world')
body.set('content', '🌎')

const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
  headers: { Accept: 'multipart/form-data' },
  method: 'POST',
  body,
})

if (response.ok) {
  const data = await response.formData()

  data.get('id') // → 1
}

arrayBuffer

function arrayBuffer(): Promise<ArrayBuffer>

Sets Accept: '*/*' in headers and parses the body as ArrayBuffer:

const buffer = await instance.get('Example.ogg').arrayBuffer()
const context = new AudioContext()
const source = new AudioBufferSourceNode(context)

source.buffer = await context.decodeAudioData(buffer)
source.connect(context.destination)
source.start()
Same code with native fetch
const response = await fetch(
  'https://upload.wikimedia.org/wikipedia/commons/c/c8/Example.ogg'
)

if (response.ok) {
  const data = await response.arrayBuffer()
  const context = new AudioContext()
  const source = new AudioBufferSourceNode(context)

  source.buffer = await context.decodeAudioData(buffer)
  source.connect(context.destination)
  source.start()
}

blob

function blob(): Promise<Blob>

Sets Accept: '*/*' in headers and parses the body as Blob:

const blob = await YF.get('https://placekitten.com/200').blob()
const image = new Image()

image.src = URL.createObjectURL(blob)
document.body.append(image)
Same code with native fetch
const response = await fetch('https://placekitten.com/200')

if (response.ok) {
  const blob = await response.blob()
  const image = new Image()

  image.src = URL.createObjectURL(blob)
  document.body.append(image)
}

void

function void(): Promise<void>

Sets Accept: '*/*' in headers and returns undefined after the request:

const nothing = await instance.post('/posts', { title: 'Hello' }).void()
Same code with native fetch
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Hello' }),
})

if (response.ok) {
  // do something
}

↕️ Jump to

🔧 Options

Accepts all the options from native fetch in the desktop browsers, or node-fetch in node.js. Additionally you can specify:

resource?: string

Part of the request URL. If used multiple times all the parts will be concatenated to final URL. The same as first argument of get, post, patch, put, delete, head.

const instance = YF.create({
  resource: 'https://jsonplaceholder.typicode.com',
})

// will me merged and send request to 'https://jsonplaceholder.typicode.com/posts'
await instance.get('/posts')

// same as
await YF.get('https://jsonplaceholder.typicode.com/posts')

// will me merged to 'https://jsonplaceholder.typicode.com/posts'
const posts = instance.extend({
  resource: '/posts',
})

// will send request to 'https://jsonplaceholder.typicode.com/posts'
const result = await posts.get().json() // → [{ id: 0, title: 'Title', ... }]
Related

base?: string

Base of a URL, use it only if you want to specify relative url inside resource. By default equals to location.origin if available. Not merged when you extend an instance. Most of the time use resource option instead.

// send a request to `new URL('/posts', location.origin)` if possible
await YF.get('/posts')

// send a request to `https://jsonplaceholder.typicode.com/posts`
await YF.get('https://jsonplaceholder.typicode.com/posts')

// send a request to `new URL('/posts', 'https://jsonplaceholder.typicode.com')`
await YF.get('/posts', { base: 'https://jsonplaceholder.typicode.com' })
Related

headers?: HeadersInit

Request headers, the same as in Fetch, except multiple headers will merge when you extend an instance.

const instance = YF.create({
  headers: { 'x-from': 'Website' },
})

// will use instance `headers`
await instance.get('https://jsonplaceholder.typicode.com/posts')

// will be merged with instance `headers`
const authorized = instance.extend({
  headers: { Authorization: 'Bearer token' },
})

// will be sent with `Authorization` and `x-from` headers
await authorized.post('https://jsonplaceholder.typicode.com/posts')
Related

json?: unknown

Body for application/json type requests, stringified with JSON.stringify and applies needed headers automatically.

await instance.patch('/posts/1', { json: { title: 'Hey' } })
Related

params?: URLSearchParams | object | string

Search params to append to the request URL. Provide an object, string, or URLSearchParams instance. The object will be stringified with serialize function.

// request will be sent to 'https://jsonplaceholder.typicode.com/posts?userId=1'
await instance.get('/posts', { params: { userId: 1 } })
Related

serialize?: (params: object): URLSearchParams | string

Custom search params serializer when object is used. Defaults to internal implementation based on URLSearchParams with better handling of array values.

import queryString from 'query-string'

const instance = YF.create({
  resource: 'https://jsonplaceholder.typicode.com',
  serialize: (params) =>
    queryString.stringify(params, {
      arrayFormat: 'bracket',
    }),
})

// request will be sent to 'https://jsonplaceholder.typicode.com/posts?userId=1&tags[]=1&tags[]=2'
await instance.get('/posts', { params: { userId: 1, tags: [1, 2] } })
Related

timeout?: number

If specified, TimeoutError will be thrown and the request will be cancelled after the specified duration.

try {
  await instance.get('/posts', { timeout: 500 })
} catch (error) {
  if (error instanceof TimeoutError) {
    // do something, or nothing
  }
}

onRequest?(url: URL, options: RequestOptions): Promise<void> | void

Request handler. Use the callback to modify options before the request or cancel it. Please, note the options here are in the final state before the request will be made. It means url is a final instance of URL with search params already set, params is an instance of URLSearchParams, and headers is an instance of Headers.

let token
const authorized = instance.extend({
  async onRequest(url, options) {
    if (!token) {
      throw new Error('Unauthorized request')
    }

    options.headers.set('Authorization', `Bearer ${token}`)
  },
})

// request will be sent with `Authorization` header resolved with async `Bearer token`.
await authorized.get('/posts')
const cancellable = instance.extend({
  onRequest(url, options) {
    if (url.pathname.startsWith('/posts')) {
      // cancels the request if condition is met
      options.signal = AbortSignal.abort()
    }
  },
})

// will be cancelled
await cancellable.get('/posts')
Related

onResponse?(response: Response): Promise<Response> | Response

Response handler, handle status codes or throw ResponseError.

const instance = YF.create({
  onResponse(response) {
    // this is the default handler
    if (response.ok) {
      return response
    }

    throw new ResponseError(response)
  },
})
Related

onSuccess?(response: Response): Promise<Response> | Response

Success response handler (usually codes 200-299), handled in onResponse.

const instance = YF.create({
  onSuccess(response) {
    // you can modify the response in any way you want
    // or even make a new request
    return new Response(response.body, response)
  },
})

onFailure?(error: ResponseError | TimeoutError | Error): Promise<Response> | Response

Throw custom error with additional data, return a new Promise with Response using request, or just submit an event to error tracking service.

class CustomResponseError extends YF.ResponseError {
  data: unknown
  constructor(response: YF.Response, data: unknown) {
    super(response)
    this.data = data
  }
}

const api = YF.create({
  resource: 'http://localhost',
  async onFailure(error) {
    if (error instanceof YF.ResponseError) {
      if (error.response.status < 500) {
        throw new CustomResponseError(
          error.response,
          await error.response.json()
        )
      }
    }

    trackError(error)
    throw error
  },
})

onJSON(input: unknown): unknown

Customize global handling of the json body. Useful for the cases when all the BE json responses inside the same shape object with .data.

const api = YF.create({
  onJSON(input) {
    // In case needed data inside object like
    // { data: unknown, status: string })
    if (typeof input === 'object' && input !== null) {
      return input.data
    }

    return input
  },
})

ResponseError

Instance of Error with failed YF.Response (based on Response) inside .response:

try {
  await instance.get('/posts').json()
} catch (error) {
  if (error instanceof YF.ResponseError) {
    error.response.status // property on Response
    error.response.options // the same as options used to create instance and make a request
  }
}

TimeoutError

Instance of Error thrown when timeout is reached before finishing the request:

try {
  await api.get('/posts', { timeout: 300 }).json()
} catch (error) {
  if (error instanceof YF.TimeoutError) {
    // do something, or nothing
  }
}

⬆️ Jump to

🔥 Migration from v1 → v2

Renamed prefixUrlresource

const api = YF.create({
-  prefixUrl: 'https://example.com'
+  resource: 'https://example.com'
})

Removed getHeaders option

Use onRequest instead:

const api = YF.create({
-  async getHeaders(url, options) {
-    return {
-      Authorization: `Bearer ${await getToken()}`,
-    }
-  },
+  async onRequest(url, options) {
+    options.headers.set('Authorization', `Bearer ${await getToken()}`)
+  },
})

CommonJS module format support dropped

Use dynamic import inside CommonJS project instead of require (or transpile the module with webpack/rollup, or vite):

- const YF = require('ya-fetch')
+ import('ya-fetch').then((YF) => { /* do something */ })

Module exports changed

The module doesn't include a default export anymore, use namespace import instead of default:

- import YF from 'ya-fetch'
+ import * as YF from 'ya-fetch'

Errors are own instances based on Error

import * as YF from 'ya-fetch'

try {
-  throw YF.ResponseError(new Response()) // notice no 'new' keyword before `ResponseError`
+  throw new YF.ResponseError(new Response())
} catch (error) {
-  if (YF.isResponseError(error)) {
+  if (error instanceof YF.ResponseError) {
      console.log(error.response.status)
  }
}

Related

Use spec compliant check for AbortError

There is no globally available AbortError but you can check .name property on Error:

try {
  await YF.get('https://jsonplaceholder.typicode.com/posts', {
    signal: AbortSignal.abort(),
  })
} catch (error) {
  if (error instanceof Error && error.name === 'AbortError') {
    /* do something or nothing */
  }
}

If you use ya-fetch only in Node.js environment, then you can import AbortError class from node-fetch module and check the error:

import { AbortError } from 'node-fetch'

try {
  await YF.get('https://jsonplaceholder.typicode.com/posts', {
    signal: AbortSignal.abort(),
  })
} catch (error) {
  if (error instanceof AbortError) {
    /* do something or nothing */
  }
}

Removed options from the second argument of onResponse, onSuccess, and onFailure

const api = YF.create({
-  async onFailure(error, options) {
-    console.log(options.headers)
-  },
+  async onFailure(error) {
+    if (error instanceof YF.ResponseError) {
+      console.log(error.response.options.headers)
+    }
+  },
})

Removed helpers

  • isResponseErrorerror instanceof YF.ResponseError
  • isTimeoutErrorerror instanceof YF.TimeoutError
  • isAbortErrorerror instanceof Error && error.name === 'AbortError'

🔗 Alternatives

  • ky - Library that inspired this one, but 3x times bigger and feature packed
  • axios - Based on old XMLHttpRequests API, almost 9x times bigger, but super popular and feature packed

MIT © John Grishin