Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@spechycom/request

A thin fetch wrapper with zero runtime dependencies. 2.6 KB minified / 1.4 KB gzipped.

The package lives in the spechycom GitHub Packages registry. Add an .npmrc to the consuming project:

@spechycom:registry=https://npm.pkg.github.com
npm i @spechycom/request

Full guide: docs/usage.md — API reference, recipes (auth refresh, uploads, cancellation, test stubs), and an Axios migration table.

import req, { create, RequestError } from '@spechycom/request'

const api = create({
  baseUrl: 'https://api.spechy.com/v1',
  headers: { authorization: `Bearer ${token}` },
  timeout: 10_000,
})

const me = await api.get<User>('/me', { params: { include: 'roles' } })
const ticket = await api.post<Ticket>('/tickets', { json: { subject: 'Hello' } })

const scoped = api.extend({ headers: { 'x-tenant': 'acme' } })

Behaviour

  • Responses are parsed by content-type: json or text. HEAD, 204 and content-length: 0 resolve to null.
  • Non-2xx responses throw RequestError with err.status, err.data (the parsed error body) and err.response.
  • Pass json and the body is serialized and content-type is set. Leave it out and body passes through untouched, so FormData/Blob set their own boundary.
  • The whole RequestInit surface stays available (signal, credentials, cache, mode, ...).
  • Need headers or full control over status? api.raw() returns the Response and never throws.

Timeout

await api.get('/report', { timeout: 3000 }) // rejects with err.name === 'TimeoutError'

Counted per attempt. Pass your own signal and the two are combined via AbortSignal.any, so you never lose the ability to cancel — a caller abort surfaces as err.name === 'AbortError'.

Retry

Two extra attempts by default, only for GET PUT DELETE HEAD OPTIONS and 408 413 429 500 502 503 504 plus network errors and timeouts. POST and PATCH are never retried, and neither is a caller abort.

await api.get('/flaky', { retry: 5 })
await api.get('/once', { retry: 0 })

Backoff is 300ms * 2^i capped at 10 s; on 429/503 a Retry-After header wins.

Hooks

const api = create({
  baseUrl,
  beforeRequest: [
    (ctx) => {
      ;(ctx.init.headers as Headers).set('x-request-id', crypto.randomUUID())
    },
  ],
  afterResponse: [
    async (res, ctx) => {
      if (res.status !== 401) return
      await refreshToken()
      return fetch(ctx.url, ctx.init) // continue with this response
    },
  ],
})
  • beforeRequest may mutate ctx.url / ctx.init; returning a Response skips fetch entirely (offline cache, test stubs).
  • afterResponse returning a Response makes the chain continue with it.
  • Hooks run on every attempt, and the retry decision looks at whatever the hooks produced last.
  • extend() concatenates hook arrays, merges headers key by key, and shallow-overrides everything else.

Development

npm test          # node tests -> build -> size gate -> real Chrome test
npm run test:node
npm run size      # exits 1 if minified > 20 KB or gzipped > 7 KB

Requires Node ≥ 20.3, Chrome ≥ 116, Safari ≥ 17.4 (for AbortSignal.any). ESM is primary, CJS ships as a second output.

About

Lightweight fetch wrapper for Spechy projects: baseUrl, JSON, timeout, retry, hooks — 2.6 KB minified

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages