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

@classytic/arc-next

v0.1.3

Published

React + TanStack Query SDK for Arc resources

Readme

@classytic/arc-next

React + TanStack Query SDK for Arc resources. Typed CRUD hooks with optimistic updates, automatic rollback, multi-tenant scoping, pagination normalization, and detail cache prefilling. No separate state management library needed.

Requires: React 19+, TanStack React Query 5+

Install

npm install @classytic/arc-next

Peer dependencies:

npm install react@^19 @tanstack/react-query@^5

Setup

Call the configuration functions once at app init (e.g., in your root providers):

import { configureClient, configureAuth } from "@classytic/arc-next/client";
import { configureToast } from "@classytic/arc-next/mutation";
import { configureNavigation } from "@classytic/arc-next/hooks";
import { toast } from "sonner";
import { useRouter } from "next/navigation";

// Required — sets the API base URL and auth mode
configureClient({
  baseUrl: process.env.NEXT_PUBLIC_API_URL!,
  authMode: "cookie", // 'cookie' for Better Auth, 'bearer' for token auth (default)
  internalApiKey: process.env.NEXT_PUBLIC_INTERNAL_API_KEY, // optional
});

// Optional — auto-inject org context into queries/mutations
configureAuth({
  getOrgId: () => activeOrgId, // return current org ID
  getToken: () => null, // null for cookie auth (token only for bearer)
});

// Optional — pluggable toast (defaults to console)
configureToast({ success: toast.success, error: toast.error });

// Optional — enables useNavigation() routing (defaults to cache-only)
configureNavigation(useRouter);

Subpath Exports

| Import | Purpose | "use client" | | ----------------------------------- | ---------------------------------------------------------------------------- | :-------------: | | @classytic/arc-next/client | configureClient, configureAuth, createClient, handleApiRequest | No | | @classytic/arc-next/api | BaseApi, createCrudApi, response types, type guards | No | | @classytic/arc-next/query | createQueryKeys, createCacheUtils, createListQuery, createDetailQuery| Yes | | @classytic/arc-next/mutation | configureToast, useMutationWithTransition, createOptimisticMutation | Yes | | @classytic/arc-next/hooks | createCrudHooks, configureNavigation | Yes | | @classytic/arc-next/query-client | getQueryClient (SSR-safe singleton) | No | | @classytic/arc-next/prefetch | createCrudPrefetcher, dehydrate (SSR prefetch) | No |

No barrel index — every file is its own entry point. Tree-shakeable (sideEffects: false).

Quick Start

1. Define API

import { createCrudApi } from "@classytic/arc-next/api";

interface Product {
  _id: string;
  name: string;
  price: number;
  organizationId: string;
}

interface CreateProduct {
  name: string;
  price: number;
}

export const productsApi = createCrudApi<Product, CreateProduct>(
  "products",
  { basePath: "/api" }
);

2. Create hooks

import { createCrudHooks } from "@classytic/arc-next/hooks";
import { productsApi } from "./products-api";

export const {
  KEYS: productKeys,
  cache: productCache,
  useList: useProducts,
  useDetail: useProduct,
  useActions: useProductActions,
  useNavigation: useProductNavigation,
} = createCrudHooks<Product, CreateProduct>({
  api: productsApi,
  entityKey: "products",
  singular: "Product",
});

3. Use in components

"use client";

export function ProductsPage() {
  const { items, pagination, isLoading } = useProducts(null, {
    organizationId: "org-123",
  }, { public: true });

  const { create, remove, isCreating } = useProductActions();

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <button
        onClick={() => create({ data: { name: "Widget", price: 9.99 } })}
        disabled={isCreating}
      >
        Add Product
      </button>
      {items.map((p) => (
        <div key={p._id}>
          {p.name} — ${p.price}
          <button onClick={() => remove({ id: p._id })}>Delete</button>
        </div>
      ))}
      {pagination && <span>{pagination.total} total</span>}
    </div>
  );
}

API Reference

configureClient(config)

configureClient({
  baseUrl: string;              // Required — API base URL
  authMode?: 'cookie' | 'bearer'; // Default: 'bearer'
  internalApiKey?: string;      // Optional — sent as x-internal-api-key header
  defaultHeaders?: Record<string, string>; // Optional — merged into every request
});

Must be called before any API requests. Throws if not configured.

configureAuth(config)

configureAuth({
  getToken?: () => string | null;   // For bearer auth — return access token
  getOrgId?: () => string | null;   // Return active organization ID
});

Auto-injects token and organizationId into queries/mutations. Hooks use the new signature (no explicit token param) — legacy signature still works.

handleApiRequest<T>(method, endpoint, options?)

Universal fetch wrapper. Handles JSON, PDF, image, CSV, and text responses.

const result = await handleApiRequest<ApiResponse<User>>("GET", "/api/users/me");
const list = await handleApiRequest<PaginatedResponse<Product>>("GET", "/api/products?page=1");

Options:

  • body — request body (auto-serializes JSON, passes FormData as-is)
  • token — Bearer token
  • organizationId — sent as x-organization-id header
  • headerOptions — additional headers merged into request
  • revalidate / tags / cache — Next.js fetch extensions

createQueryString(params)

MongoKit-compatible query string builder:

  • Arrays → field[in]=a,b,c
  • populateOptionspopulate[path][select]=field1,field2
  • nullfield=null

createCrudApi<TDoc, TCreate, TUpdate>(entity, config?)

Creates a typed API client instance.

const api = createCrudApi<Product, CreateProduct>("products", {
  basePath: "/api",       // default: "/api/v1"
  defaultParams: { limit: 20 },
  cache: "no-store",      // default
  headers: {              // optional — sent with every request from this instance
    "x-arc-scope": "platform",  // e.g. for superadmin elevation
  },
});

Methods:

| Method | Signature | |---|---| | getAll | ({ token?, organizationId?, params? }) → PaginatedResponse<T> | | getById | ({ id, token?, organizationId?, params? }) → ApiResponse<T> | | create | ({ data, token?, organizationId? }) → ApiResponse<T> | | update | ({ id, data, token?, organizationId? }) → ApiResponse<T> | | delete | ({ id, token?, organizationId? }) → DeleteResponse | | search | ({ searchParams?, params?, token?, organizationId? }) → PaginatedResponse<T> | | findBy | ({ field, value, operator?, token?, organizationId? }) → PaginatedResponse<T> | | request | (method, endpoint, { data?, params?, token? }) → T |

prepareParams(params) — processes query params: critical filters (organizationId, ownerId) preserved as null, arrays → field[in], pagination parsed to int.

createCrudHooks<T, TCreate, TUpdate>(config)

Factory that returns everything you need:

const { KEYS, cache, useList, useDetail, useActions, useNavigation } =
  createCrudHooks<Product, CreateProduct>({
    api: productsApi,       // from createCrudApi()
    entityKey: "products",  // TanStack Query key prefix
    singular: "Product",    // for toast messages
    defaults: {             // optional
      staleTime: 60_000,
      messages: { createSuccess: "Product added!" },
    },
    callbacks: {            // optional
      onCreate: { onSuccess: (data) => console.log("Created:", data) },
    },
  });

Returned hooks:

useList(token, params?, options?)

const { items, pagination, isLoading, isFetching, refetch } = useList(
  token,
  { organizationId: "org-123", status: "active" },
  { public: true, staleTime: 30_000, prefillDetailCache: true }
);
  • Auto-scopes query keys by organizationId (tenant vs super-admin)
  • Normalizes pagination from docs/data/items/results formats
  • Prefills detail cache from list results (skips re-fetch on navigate)
  • options.public: true — enables query without token

useDetail(id, token, options?)

const { item, isLoading } = useDetail(productId, token, {
  organizationId: "org-123",
});
  • Disabled when id is null (conditional fetching)
  • Extracts item from { data: T } wrapper

useActions()

const { create, update, remove, isCreating, isUpdating, isDeleting, isMutating } =
  useActions();

// All mutations have optimistic updates + automatic rollback on error
await create({ data: { name: "New" }, organizationId: "org-123" });
await update({ id: "123", data: { name: "Updated" } });
await remove({ id: "123" });

// Per-call callbacks
await create(
  { data: { name: "New" } },
  { onSuccess: (item) => navigate(`/products/${item._id}`) }
);
  • Create — optimistic: prepends to list with temp ID
  • Update — optimistic: patches item in list + detail cache
  • Delete — optimistic: removes from list + detail cache
  • All roll back automatically on error

useNavigation()

const navigate = useNavigation();
navigate(`/products/${id}`, product);              // push + cache prefill
navigate(`/products/${id}`, product, { replace: true }); // replace

Sets detail cache before navigation (instant page load, no loading spinner). Requires configureNavigation(useRouter) — without it, only sets cache (no routing).

Query Keys (KEYS)

KEYS.all                          // ["products"]
KEYS.lists()                      // ["products", "list"]
KEYS.list(params)                 // ["products", "list", params]
KEYS.details()                    // ["products", "detail"]
KEYS.detail(id)                   // ["products", "detail", id]
KEYS.custom("stats", orgId)       // ["products", "stats", orgId]
KEYS.scopedList("tenant", params) // ["products", "list", { _scope: "tenant", ...params }]

Cache Utilities (cache)

await cache.invalidateAll(queryClient);
await cache.invalidateLists(queryClient);
await cache.invalidateDetail(queryClient, id);
cache.setDetail(queryClient, id, data);
cache.getDetail(queryClient, id);       // T | undefined
cache.removeDetail(queryClient, id);

getQueryClient(overrides?)

SSR-safe singleton. Server: new per request. Browser: reuses singleton.

import { getQueryClient } from "@classytic/arc-next/query-client";
import { QueryClientProvider } from "@tanstack/react-query";

function Providers({ children }) {
  const queryClient = getQueryClient();
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

Defaults: staleTime: 5min, gcTime: 30min, retry: 0, refetchOnWindowFocus: false.

SSR Prefetch (Server Components)

Pre-populate the query cache on the server to avoid loading spinners:

// products-prefetch.ts
import { createCrudPrefetcher } from "@classytic/arc-next/prefetch";
import { productsApi } from "@/api/products-api";

export const productsPrefetcher = createCrudPrefetcher(productsApi, "products");
// app/products/page.tsx (server component)
import { getQueryClient } from "@classytic/arc-next/query-client";
import { dehydrate } from "@classytic/arc-next/prefetch";
import { HydrationBoundary } from "@tanstack/react-query";
import { productsPrefetcher } from "@/prefetch/products-prefetch";

export default async function ProductsPage() {
  const queryClient = getQueryClient();
  await productsPrefetcher.prefetchList(queryClient, { limit: 20 });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <ProductsList />
    </HydrationBoundary>
  );
}

Methods: prefetchList(queryClient, params?, options?), prefetchDetail(queryClient, id, options?)

Custom Mutations

For operations beyond CRUD (publish, schedule, upload):

useMutationWithTransition(config)

Mutation + React 19 useTransition for smooth cache invalidation:

import { useMutationWithTransition } from "@classytic/arc-next/mutation";

export function usePublishPost() {
  return useMutationWithTransition({
    mutationFn: (id: string) =>
      postsApi.request("POST", `${postsApi.baseUrl}/${id}/publish`),
    invalidateQueries: [postKeys.all],
    messages: { success: "Published!", error: "Failed to publish" },
    useTransition: true, // default
    showToast: true, // default
  });
}

Returns: { mutate, mutateAsync, isPending, isSuccess, isError, error, data, reset }

useMutationWithOptimistic(config)

Mutation + optimistic updates + automatic rollback:

import { useMutationWithOptimistic } from "@classytic/arc-next/mutation";

export function useToggleFavorite() {
  return useMutationWithOptimistic({
    mutationFn: ({ id, isFav }) =>
      api.request("PATCH", `/api/products/${id}`, {
        data: { favorite: !isFav },
      }),
    queryKeys: [productKeys.lists()],
    optimisticUpdate: (old, { id, isFav }) =>
      updateListCache(old, (items) =>
        items.map((i) => (getItemId(i) === id ? { ...i, favorite: !isFav } : i))
      ),
    messages: { success: "Updated!" },
  });
}

Query Config Presets

import { QUERY_CONFIGS } from "@classytic/arc-next/mutation";

// Use in useList options:
useProducts(token, {}, { ...QUERY_CONFIGS.realtime });

| Preset | staleTime | refetchInterval | | ---------- | ----------- | ------------------ | | realtime | 20s | 30s | | frequent | 1min | — | | stable | 5min | — | | static | 10min | — |

Low-Level Utilities

updateListCache(listData, updater)

Transforms list cache regardless of format (docs[], data[], items[], results[], or raw array):

import { updateListCache } from "@classytic/arc-next/query";

queryClient.setQueryData(KEYS.lists(), (old) =>
  updateListCache(old, (items) => items.filter((i) => i.status !== "archived"))
);

getItemId(item)

Extracts _id or id from any item. Returns string | null.

Multi-Client (Multiple APIs)

By default, configureClient() sets a single global baseUrl. Use createClient() when your app talks to multiple backends.

Create isolated clients

import { createClient } from "@classytic/arc-next/client";
import { toast } from "sonner";
import { useRouter } from "next/navigation";

const analyticsClient = createClient({
  baseUrl: "https://analytics.example.com",
  internalApiKey: "analytics-key",
  toast: { success: toast.success, error: toast.error },
  navigation: useRouter,
});

Use with createCrudApi

Pass client in the config — requests go through the client's baseUrl instead of the global one:

const eventsApi = createCrudApi("events", {
  basePath: "/api",
  client: analyticsClient,
});

Use with createCrudHooks

Pass client — toast and navigation use the client's handlers instead of globals:

const { useList, useActions } = createCrudHooks({
  api: eventsApi,
  entityKey: "events",
  singular: "Event",
  client: analyticsClient,
});

Direct requests

const data = await analyticsClient.request("GET", "/api/stats");
const result = await analyticsClient.request("POST", "/api/events", {
  body: { type: "page_view" },
});

Response Types

import type {
  ApiResponse,                   // { success, data?, message? }
  PaginatedResponse,             // OffsetPaginationResponse | KeysetPaginationResponse | AggregatePaginationResponse
  OffsetPaginationResponse,      // { docs[], page, limit, total, pages, hasNext, hasPrev }
  KeysetPaginationResponse,      // { docs[], limit, hasMore, next }
  AggregatePaginationResponse,   // same shape as offset
  DeleteResponse,                // { success, deleted, id?, soft?, message? }
} from "@classytic/arc-next/api";

// Type guards
import {
  isOffsetPagination,
  isKeysetPagination,
  isAggregatePagination,
} from "@classytic/arc-next/api";

Error Handling

All API errors throw ArcApiError:

import { ArcApiError } from "@classytic/arc-next/client";

try {
  await productsApi.create({ data: { name: "" } });
} catch (err) {
  if (err instanceof ArcApiError) {
    console.log(err.status);  // HTTP status code
    console.log(err.message); // Error message from server
  }
}

Common Patterns

Multi-tenant data fetching

// organizationId in params → scoped query key → isolated cache per tenant
const { items } = useProducts(token, { organizationId: currentOrg });

Public endpoints (no auth)

const { items } = useProducts(null, {}, { public: true });

Conditional fetching

const { item } = useProduct(selectedId, token); // disabled when selectedId is null

Per-call callbacks

await create(
  { data: formData, organizationId: org },
  {
    onSuccess: (product) => router.push(`/products/${product._id}`),
    onError: (err) => setFieldErrors(err),
  }
);

Navigate with cache prefill

const navigate = useProductNavigation();
// Prefills detail cache → no loading spinner on detail page
navigate(`/products/${product._id}`, product);

Per-instance headers

// All requests from this API include x-arc-scope header
const adminApi = createCrudApi("users", {
  headers: { "x-arc-scope": "platform" },
});

Features

  • CRUD FactorycreateCrudApi + createCrudHooks generates typed API clients and React Query hooks
  • Optimistic Updates — Create, update, delete with instant UI feedback and automatic rollback
  • Multi-Tenant ScopingorganizationId in headers + scoped query keys
  • Pagination Normalization — Handles docs/data/items/results response formats, offset/keyset/aggregate pagination
  • Detail Cache Prefilling — List results auto-populate detail query cache
  • React 19 TransitionsuseMutationWithTransition wraps invalidation in startTransition
  • Cookie & Bearer AuthauthMode: 'cookie' for Better Auth, 'bearer' for token auth
  • SSR PrefetchcreateCrudPrefetcher + dehydrate for server component data loading
  • Multi-ClientcreateClient() for multiple API backends side by side
  • Pluggable ToastconfigureToast() — use sonner, react-hot-toast, or anything
  • Pluggable NavigationconfigureNavigation() — use Next.js, React Router, or any router
  • SSR-Safe QueryClientgetQueryClient() — singleton in browser, new per request on server
  • Per-Instance Headersconfig.headers on createCrudApi merged into every request
  • Query Config PresetsQUERY_CONFIGS.realtime/frequent/stable/static
  • Framework-Agnostic — No hard dependency on Next.js
  • Tree-ShakeablesideEffects: false, flat files, no barrels

License

MIT