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

@construkt-kit/api

v0.4.0

Published

HTTP client, query factories, typed errors, and generated API types for Construkt Kit apps

Readme

@construkt-kit/api

Configuration for the Kubb-generated HTTP client, typed error classes, and data-table types for Construkt Kit frontend apps.

Exports

Client

| Export | Description | | ---------------------------- | ---------------------------------------------------------------------- | | configureApiClient | Points a generated Kubb client at an API; returns a disposer | | fetchTransport | Fetch transport used by default — JSON, text, SSE and binary responses | | ApiClient | Structural type of the generated client this package drives | | ApiRequest | Resolved request handed to a transport | | ApiTransport | Transport signature (ApiRequest → ApiTransportResult) | | ApiTransportResult | What a transport returns (parsed body plus native request/response) | | ApiResponseError | Non-2xx response the error interceptor maps | | ConfigureApiClientOptions | { baseURL, getToken, transport? } |

Error Classes

| Export | Description | | ------------------- | ---------------------------------------------- | | ApiError | Base error class (status, code, message) | | ValidationError | 422 error (extends ApiError) | | NotFoundError | 404 error (extends ApiError) | | UnauthorizedError | 401 error (extends ApiError) | | toApiError | Maps a status onto the narrowest class | | toErrorMessage | Picks the message out of an error body | | ApiErrorResponse | Interface — { detail?, title?, Message? } |

Data-Table Types

| Export | Description | | ------------------- | ------------------------------------------------- | | DataTableFilters | Record<string, string[] \| undefined> | | DataTableSortType | "asc" \| "desc" \| "" | | DataTableParams | { page, pageSize, orderBy, orderType, filters } |

Usage

Kubb generates its own fetch runtime into <output>/.kubb/client.ts. This package configures that client; it never replaces it.

import { configureApiClient } from "@construkt-kit/api";

import { client } from "@/api/gen/.kubb/client";

const dispose = configureApiClient(client, {
  baseURL: "https://api.example.com",
  getToken: () => authToken,
});

Generated calls and hooks then work unchanged:

import { ApiError, NotFoundError } from "@construkt-kit/api";

// Hook: resolves to the body, errors surface on `error`
const { data: project, error } = useGetApiProjectsId({ path: { id } });

if (error instanceof NotFoundError) {
  /* 404 */
}
if (error instanceof ApiError) {
  /* any API error */
}

// Call function: awaitable, `.unwrap()` for the body alone
const project = await getApiProjectsId({ path: { id } }).unwrap();

Key Patterns

Token callback

getToken is a synchronous callback (() => string | null | undefined) read on every request, so token refresh needs no reconfiguration. The resulting Authorization: Bearer header replaces one set through client.setConfig({ headers }) whatever its casing, and when the callback returns nothing the header is removed — so a logout stops sending the old token.

Sync/async gap: AuthProvider.getToken from @construkt-kit/pages returns Promise<string | null>. Cache the token in the app and hand over the cached value:

let cachedToken: string | null = null;
// Update cachedToken when auth state changes
configureApiClient(client, { baseURL, getToken: () => cachedToken });

Disposer

configureApiClient returns a function that removes the interceptors it registered. Call it before re-configuring the same client, so a hot reload does not stack interceptors.

Response bodies

fetchTransport decides how to read a response:

  • JSON content types are parsed; an unparseable body is passed through as text
  • text/* is read as text, text/event-stream as the raw stream
  • every other media type becomes a Blob — the Kubb runtime would decode it as text
  • 204/205 and empty bodies resolve to undefined
  • an explicit per-operation responseType (blob, arraybuffer, stream, text, json) wins for a 2xx response; an error body is always read by its content type, so its message survives

Only 2xx counts as success, so a 304 raises an ApiError rather than resolving empty. Send conditional requests (If-None-Match, If-Modified-Since) only where the caller handles that.

Generated hooks resolve to the body alone. For a download, call the generated function directly and read the filename off the response — the body is already consumed, so use data, not response.blob():

import { saveBlob } from "@construkt-kit/utils";

const { data, response } = await postApiExportsExcel({ body });

saveBlob(data as Blob, response.headers.get("Content-Disposition"), "export.xlsx");

Error hierarchy

All errors extend ApiError, which uses Object.setPrototypeOf(this, new.target.prototype) — required for instanceof checks in transpiled TypeScript. Subclasses hardcode their status: ValidationError → 422, NotFoundError → 404, UnauthorizedError → 401.

Non-2xx responses are raised through an error interceptor, so instanceof works directly. This applies while throwOnError is on, which is the default and what the generated hooks use; a call that opts out with throwOnError: false gets the raw body on error and can map it with toApiError and toErrorMessage:

try {
  await apiCall();
} catch (error) {
  if (error instanceof NotFoundError) return null;
  if (error instanceof UnauthorizedError) return signOut();
  if (error instanceof ApiError) reportError(error.code, error.message);
}

The message is the body's detail, then title (an RFC 9457 problem document), then a legacy Message, falling back to "An error has occurred.". code is a stable screaming-snake identifier (NOT_FOUND, VALIDATION_ERROR, CONFLICT), derived from the status alone — the status text is empty over HTTP/2 — as its registered reason phrase, or HTTP_<status> for an unregistered one. Network failures reject with the underlying TypeError, not an ApiError.

Custom transport

Pass transport to wrap fetchTransport — telemetry, retries, or response fixups:

configureApiClient(client, {
  baseURL,
  getToken,
  transport: async (request) => {
    const startedAt = performance.now();
    const result = await fetchTransport(request);
    track("api_call", {
      endpoint: new URL(request.url).pathname,
      durationMs: performance.now() - startedAt,
    });

    return result;
  },
});

Kubb codegen integration

Consuming apps generate typed API code with createKubbConfig() from @construkt-kit/config/kubb:

| Output dir | Contents | | -------------- | ------------------------------------------------------- | | dtos/ | TypeScript types generated from OpenAPI schemas | | calls/ | One function per operation, returning the full result | | hooks/ | React Query hooks grouped by API path | | .kubb/ | The bundled fetch runtime (client, createClient) |

Every generated function takes one grouped options object — { path, query, body, headers } — and resolves to { status, data, error, request, response }. Add .unwrap() for the body alone, which is what the hooks do.

Options (via createKubbConfig()):

  • inputPath — OpenAPI spec location (default: ./src/api/openapi.json)
  • outputPath — generated output root (default: ./src/api/gen)
  • overrides — merged over the generated config

CLI: construkt-kit-api-gen

Fetches an OpenAPI spec from a running API, runs Kubb codegen, and deletes the downloaded spec. It runs the app's own kubb, so install kubb next to the Kubb plugins createKubbConfig() uses; both must come from the same Kubb release.

Usage

# Uses API_URL env var or specUrl from config
npx construkt-kit-api-gen

# Override the API base URL
npx construkt-kit-api-gen --url https://api.example.com

# Use a custom config file (default: api.config.ts)
npx construkt-kit-api-gen --config my-api.config.ts

# Generate from a spec on disk; nothing is downloaded or deleted
npx construkt-kit-api-gen --input ./spec/openapi.json

Config file (api.config.ts)

import { createKubbConfig } from "@construkt-kit/config/kubb";

export const specUrl = "https://api.example.com";
export default createKubbConfig();

URL resolution priority

  1. --url CLI flag
  2. API_URL environment variable
  3. specUrl named export from config file

The spec is fetched from {baseUrl}/openapi/v1.json, saved to the config's input path, passed to Kubb, then deleted. Generation problems are printed and exit the process with code 1.

http and https base URLs both work, and redirects are followed. HTTPS certificates are verified: for a dev certificate the OS trusts (dotnet dev-certs https --trust), run with NODE_OPTIONS=--use-system-ca; otherwise use the API's http URL or set NODE_EXTRA_CA_CERTS.

Upgrading from 0.3

  • toApiError(status, message) drops statusText. code comes from the status alone, so a status the server answered with a custom reason phrase gets its registered one
  • configureApiClient strips trailing slashes from baseURL
  • An explicit responseType applies to successful responses only; error bodies are parsed by content type so their detail reaches the message
  • construkt-kit-api-gen verifies HTTPS certificates and keeps the base URL's path
  • construkt-kit-api-gen reads api.config.ts in projects without "type": "module", where it used to fall back to the defaults, and deletes a partly downloaded spec