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

@vinicunca/request

v0.1.0

Published

Generic typed request clients: oRPC and ofetch-based REST, each with TanStack Query helpers.

Readme

@vinicunca/request

Generic, typed request clients:

| Entry | What it is | |---|---| | @vinicunca/request/orpc | a typed oRPC client for any contract or router | | @vinicunca/request/orpc-query | TanStack Query utils for that client | | @vinicunca/request/http | a REST client on ofetch with an interceptor chain and presets | | @vinicunca/request/http-query | TanStack Query options builders for the REST client |

pnpm add @vinicunca/request
pnpm add ofetch                 # for ./http and ./http-query
pnpm add @tanstack/vue-query    # (or any TanStack adapter) for the *-query entries

Nothing is sent with cookies unless you pass credentials: 'include'.

oRPC

import type { MyContract } from 'my-api-contract';
import { createRpcClient, isDefinedError, safe, type ContractClient } from '@vinicunca/request/orpc';
import { createRpcQueryUtils } from '@vinicunca/request/orpc-query';

export const client = createRpcClient<ContractClient<MyContract>>({
  url: 'https://api.example.com/api/rpc',
  credentials: 'include',                    // cross-origin cookie auth
  headers: async () => ({ 'x-app': 'web' }), // or a bearer token
});
export const orpc = createRpcQueryUtils(client, { path: ['my-api'] });

const [error, todo] = await safe(client.todo.get({ id }));
if (isDefinedError(error) && error.code === 'NOT_FOUND') { /* … */ }

useQuery(orpc.todo.list.queryOptions({ input: { page: 1 } }));

For router-inferred APIs pass the router client type instead: createRpcClient<RouterClient<typeof router, RpcClientContext>>(…).

Streams: pass context: LIVE_RETRY to reconnect after network drops or 5xx and resume from the last event id (a 4xx stops the stream).

With skipLibCheck: false, also install @opentelemetry/api (types only; oRPC's shared types reference it).

HTTP

import {
  createHttpClient,
  envelopeInterceptor,
  errorMessageInterceptor,
  refreshTokenInterceptor,
} from '@vinicunca/request/http';

export const http = createHttpClient({
  baseURL: 'https://api.example.com',
  timeout: 10_000,         // default; combined with any signal you pass
  responseReturn: 'data',  // 'raw' | 'body' (default) | 'data'
});

http.addRequestInterceptor((context) => {
  context.options.headers.set('Authorization', `Bearer ${store.accessToken}`);
});
http.addResponseInterceptor(envelopeInterceptor({ codeField: 'code', dataField: 'data', successCode: 0 }));
http.addResponseInterceptor(refreshTokenInterceptor({
  client: http,
  // `skipAuthRefresh: true` is required: without it, the refresh call's own
  // 401 would re-enter this interceptor and await the very attempt it's
  // part of, deadlocking forever.
  refresh: () => http.post<string>('/auth/refresh', undefined, { skipAuthRefresh: true, responseReturn: 'data' }),
  applyToken: (context, token) => context.options.headers.set('Authorization', `Bearer ${token}`),
  onAuthFailure: () => logout(),
}));
http.addResponseInterceptor(errorMessageInterceptor({
  notify: (message) => toast.error(message),
  messages: { default: t('errors.default') },   // override any English default
}));

const users = await http.get<User[]>('/users', { query: { page: 1 } });
await http.post('/users', { name: 'Ana' });
  • Install presets in this order: envelope → refresh token → error message, so only errors nobody recovered from show a message.
  • responseReturn: raw returns { status, headers, data, request, response }; body returns the parsed body; data returns the envelope's payload (with envelopeInterceptor), throwing HttpError{ kind: 'envelope' } on a bad code.
  • Errors: every request rejects with HttpError (kind: network, timeout, abort, http, envelope; plus status, code, data, request). Transport and HTTP failures reject with HttpError; exceptions thrown by your own interceptors propagate unchanged (not wrapped as HttpError).
  • Interceptors: request interceptors may mutate context.options; response interceptors are a promise chain — fulfilled may transform, rejected may recover by returning a value (or an HttpResponse). Both add* functions return a remover. A fulfilled handler must return the response it received (or a spread of it, { ...response, data }) — a freshly built object loses the internal brand, so later fulfilled handlers are skipped and responseReturn no longer unwraps it.
  • Token refresh: concurrent 401s share one refresh(); each request is retried once; a second 401, a failed refresh or enabled: false calls onAuthFailure and rejects. A skipAuthRefresh request is never retried and also calls onAuthFailure on a 401 — deferred until an in-flight refresh succeeds, and skipped if that refresh fails, which is already reported once.
  • TypeScript: with skipLibCheck: false, also install undici and @types/node (types only; ofetch's type declarations import undici, whose own types need @types/node).

Query strings

query is handed to ofetch by default (ufo): flat values and repeated arrays (ids=1&ids=2). Nested objects become JSON there and Dates are quoted, so for qs-style APIs set arrayFormat:

| arrayFormat | { ids: [1, 2], user: { name: 'Ana' } } | |---|---| | repeat | ids=1&ids=2&user[name]=Ana | | brackets | ids[]=1&ids[]=2&user[name]=Ana | | indices | ids[0]=1&ids[1]=2&user[name]=Ana | | comma | ids=1,2&user[name]=Ana |

Arrays of objects always use indices (items[0][n]=a), Dates are ISO strings, null is key=, undefined is omitted. Values are encoded by URLSearchParams (spaces as +, brackets as %5B%5D). Use querySerializer: (query) => string for anything else; a string or URLSearchParams query is used as-is.

Uploads: body: formData. Downloads: responseType: 'blob'. For anything else, http.raw is the configured ofetch instance — it only inherits baseURL (and the injected fetch), not the client's headers, credentials, timeout or interceptors.

HTTP + TanStack Query

import { createHttpQueryUtils } from '@vinicunca/request/http-query';

const api = createHttpQueryUtils(http, { key: ['my-api'] });

const products = useQuery(computed(() => api.get<ProductPage>('/products').queryOptions({
  query: { page: page.value },
  placeholderData: keepPreviousData,
  select: (result) => result.items,
})));

const create = useMutation(api.post<Product, NewProduct>('/products').mutationOptions({
  onSuccess: () => queryClient.invalidateQueries({ queryKey: api.get('/products').key() }),
}));

api.delete<void, { id: number }>((variables) => `/products/${variables.id}`)
  .mutationOptions({ body: () => undefined });
  • Keys: [...key, 'GET', path, { query }]; api.key() and api.get(path).key() match everything / every query of a path.
  • Request options (query, headers, credentials, timeout, responseReturn, responseType, arrayFormat, querySerializer, parseResponse) and TanStack options share one object. retry, retryDelay and meta belong to TanStack here; set HTTP retries on the client. Note: queryOptions({...}) doesn't reject unknown keys — a typo like quer for query passes through silently, same as oRPC's utils.
  • Cancelled or superseded queries abort the underlying fetch.
  • TanStack retries queries 3× by default, and each attempt runs the full interceptor chain (toasts, refresh), so a persistent 4xx can notify repeatedly. Pass a retry that only retries server errors, e.g. retry: (count, error) => !(isHttpError(error) && error.status !== undefined && error.status < 500) && count < 3.