npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@codecraftkit/fetch

v0.4.0

Published

Native fetch-based HTTP client for Next.js (Data Cache aware) with axios-style interceptors, multipart/FormData support, dual call/request API, injectable token and logger.

Readme

@codecraftkit/fetch

Native fetch-based HTTP client for Next.js App Router. Forwards Data Cache options (cache, next: { revalidate, tags }) and AbortSignal per call. Pluggable token resolver and logger. Zero runtime dependencies.

Install

npm install @codecraftkit/fetch

Requires Node >=18 (uses global fetch).

Usage

Server components (App Router)

import 'server-only'
import { cookies } from 'next/headers'
import { createFetchClient } from '@codecraftkit/fetch'

export const api = createFetchClient({
  baseUrl: process.env.API_URL,
  secret: process.env.API_SECRET,
  getToken: async () => (await cookies()).get('token')?.value
})

// Forward Data Cache controls per call
export const getBrokers = (q) => api.call(`/brokers?${q}`, null, { cache: 'no-store' })
export const getStats   = (q) => api.call(`/stats?${q}`, null, { next: { revalidate: 60, tags: ['stats'] } })

Client components

'use client'
import { createFetchClient } from '@codecraftkit/fetch'

const api = createFetchClient({
  baseUrl: process.env.NEXT_PUBLIC_API_URL,
  getToken: () => document.cookie.match(/token=([^;]+)/)?.[1]
})

Options

| Option | Type | Default | Description | |---|---|---|---| | baseUrl | string | required | Prepended to every path passed to call(). | | secret | string | — | Sent as x-secret header on every request. | | getToken | () => string \| Promise<string> | — | Resolves the auth token per request. Called on each call(). | | headersType | 'x-token' \| 'bearer' | 'x-token' | How to attach the token. bearer sets Authorization: Bearer <token>. | | defaultHeaders | object | {} | Extra headers merged into every request. | | logger | { debug, error } | no-op | Inject console to enable debug logs. |

Per-call config

api.call(path, data, {
  method: 'POST',           // default GET
  headers: { ... },         // merged on top of defaults
  cache: 'no-store',        // Next.js Data Cache
  next: { revalidate, tags },
  signal: abortController.signal,
  asBlob: false             // force blob() regardless of content-type
})

Verb shortcuts:

api.get(path, config)
api.post(path, data, config)
api.put(path, data, config)
api.delete(path, data, config)

call vs request

The client exposes two entry points sharing the same interceptor pipeline:

| Method | Returns | Use when | |--------|---------|----------| | api.call(path, data, config) (and verb shortcuts) | parsed body only | You only need the response data (95% of cases) | | api.request(path, data, config) | { data, status, headers, response, config } | You need status code or response headers (pagination, ETag, rate limits) |

// Common case
const users = await api.get('/users')

// Need pagination headers
const { data: users, headers } = await api.request('/users')
const total = headers.get('x-total-count')

// Need status code
const { data, status } = await api.request('/users', { id: 1 }, { method: 'POST' })
if (status === 201) toast.success('Created')

Both methods run request and response interceptors identically. The only difference is whether the final result is unwrapped to .data (call) or left as the full shape (request).

Response handling

  • application/json → parsed object
  • application/pdf, application/octet-stream, xlsxBlob
  • asBlob: trueBlob regardless of content-type
  • Non-2xx → throws Error with message from JSON message/error or text body

Sending raw bodies (multipart, blob, url-encoded, binary)

The client auto-detects raw body types and skips JSON serialization. The default Content-Type: application/json is stripped so the runtime can set the correct header (with boundary for multipart, blob type for Blob, etc.).

Multipart FormData

const form = new FormData()
form.append('name', 'doc.pdf')
form.append('file', fileBlob, 'doc.pdf')

await api.post('/upload', form)
// → Content-Type: multipart/form-data; boundary=... (set by runtime)

Never set Content-Type manually for FormData — the boundary is auto-generated and a manual header breaks parsing on the server. The library strips it even if you try to set it.

Blob / File

const blob = new Blob([data], { type: 'image/png' })
await api.post('/upload', blob)
// → Content-Type: image/png (from blob.type)

// Override:
await api.post('/upload', blob, { headers: { 'Content-Type': 'application/octet-stream' } })

URLSearchParams (form-urlencoded)

await api.post('/form', new URLSearchParams({ a: '1', b: '2' }))
// → Content-Type: application/x-www-form-urlencoded;charset=UTF-8

ArrayBuffer / TypedArray / ReadableStream

Pass-through without serialization. You must set Content-Type explicitly — the runtime cannot infer it.

await api.post('/binary', new Uint8Array([1, 2, 3]), {
  headers: { 'Content-Type': 'application/octet-stream' }
})

Supported raw types

| Type | Content-Type behavior | |------|----------------------| | FormData | Always stripped (runtime sets multipart + boundary) | | Blob / File | Default JSON stripped; runtime uses blob.type; user override wins | | URLSearchParams | Default JSON stripped; runtime sets form-urlencoded | | ArrayBuffer, TypedArray | Default JSON stripped; user must set Content-Type | | ReadableStream | Default JSON stripped; user must set Content-Type | | string | Default JSON kept (assumes JSON string) | | plain object / array | JSON.stringify + Content-Type: application/json |

Interceptors

Axios-compatible API. Use interceptors.request.use to mutate the outgoing request and interceptors.response.use to transform the response or recover from errors. Both return an id for eject().

Request interceptor

const api = createFetchClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL })

api.interceptors.request.use(async (config) => {
  try {
    const raw = window.localStorage.getItem('organization-tokens')
    const store = raw ? JSON.parse(raw) : null

    if (store) {
      const state = store.state
      config.headers['x-organization-id'] = state?.organizationToken
      config.headers['x-organization-app-integration-id'] = state?.organizationAppIntegrationToken
    }
  } catch (e) {}

  return config
})

The config object exposes url, method, headers, data, cache, next, signal, asBlob. Mutate and return it, or return a new object. Interceptors run after getToken, so they can override the auth header.

Response interceptor

Handlers receive a wrapped object { data, status, headers, response, config } (axios-shaped). Mutate data to transform what the caller receives. The library returns result.data after the chain runs.

api.interceptors.response.use(
  (res) => {
    res.data = res.data.payload // unwrap envelope
    return res
  },
  (error) => { throw error }
)

Error recovery and refresh-token retry

The error handler receives an Error augmented with status, response, and config. Returning a value recovers the call; throwing forwards to the next error handler.

api.interceptors.response.use(null, async (error) => {
  if (error.status !== 401) throw error
  await refreshToken()
  return api.call(error.config.url.replace(baseUrl, ''), error.config.data, {
    method: error.config.method
  })
})

Eject

const id = api.interceptors.request.use(fn)
api.interceptors.request.eject(id)

Why this and not axios?

Native fetch integrates with the Next.js Data Cache. Axios does not. Forwarding cache, next, and signal lets each caller opt into caching strategies without monkey-patching the client.

License

MIT