@construkt-kit/api
v0.2.3
Published
HTTP client, query factories, typed errors, and generated API types for Construkt Kit apps
Readme
@construkt-kit/api
HTTP client, typed error classes, and data-table types for Construkt Kit frontend apps.
Exports
Client
| Export | Description |
| --------------------- | --------------------------------------------------------------------- |
| createApiClient | Factory — creates fetch-based HTTP client with Bearer token injection |
| setApiConfig | Configure the Kubb client (base URL, etc.) |
| Client | HTTP client type (re-exported from @kubb/plugin-client) |
| RequestConfig | Request configuration type |
| ResponseConfig | Response configuration type |
| ResponseErrorConfig | Error response configuration type |
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) |
| ApiErrorResponse | Interface — { Message: string } |
Data-Table Types
| Export | Description |
| ------------------- | ------------------------------------------------- |
| DataTableFilters | Record<string, string[] \| undefined> |
| DataTableSortType | "asc" \| "desc" \| "" |
| DataTableParams | { page, pageSize, orderBy, orderType, filters } |
Usage
import { ApiError, NotFoundError, createApiClient } from "@construkt-kit/api";
import type { DataTableParams } from "@construkt-kit/api";
if (error instanceof NotFoundError) {
/* 404 */
}
if (error instanceof ApiError) {
/* any API error */
}Key Patterns
Token callback
createApiClient(getToken) accepts a synchronous callback (() => string | null | undefined), not a static token. The token is fetched at call time (not client creation), supporting token refresh.
Sync/async gap:
AuthProvider.getTokenfrom@construkt-kit/pagesreturnsPromise<string | null>, butcreateApiClientexpects a sync getter. The recommended pattern is to cache the token synchronously in the app and pass the cached value:let cachedToken: string | null = null; // Update cachedToken when auth state changes const client = createApiClient(() => cachedToken);
Binary responses
Non-JSON/text responses (Excel, PDF exports) return a Response-like object:
{ blob: () => Promise<Blob>, headers: Headers }Pair with saveBlobResponse() or downloadFile() from @construkt-kit/utils for file downloads.
Error hierarchy
All errors extend ApiError which uses Object.setPrototypeOf(this, new.target.prototype) — required for proper instanceof checks in transpiled TypeScript. Subclasses hardcode their status: ValidationError → 422, NotFoundError → 404, UnauthorizedError → 401.
createApiClient classifies non-2xx responses onto the narrowest class available — 401 → UnauthorizedError, 404 → NotFoundError, 422 → ValidationError, anything else → ApiError. So instanceof works directly:
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);
}code is a stable screaming-snake identifier (NOT_FOUND, VALIDATION_ERROR, INTERNAL_SERVER_ERROR), derived from the status text when there is no dedicated subclass.
Param normalization
createApiClient converts config.params to URLSearchParams. Rules:
undefinedvalues are omitted; no?is appended when nothing survivesnullbecomes the string"null"- Arrays are repeated per element (
ids=1&ids=2) Datevalues become ISO strings; other objects are JSON-encoded- Primitives are stringified
Headers and body
Headers layer in this order, later wins: setApiConfig({ headers }), the per-request
headers (record or tuple form), then Authorization from getToken.
FormData,URLSearchParams,Blob,ArrayBuffer, typed arrays and streams pass through untouched; forFormDatatheContent-Typeheader is removed so fetch can set the boundary- With an
application/x-www-form-urlencodedcontent type, a plain object is form-encoded with the param rules above, except thatnullis omitted - A string body is sent as-is when a non-JSON content type is given
- Anything else is JSON-encoded (
bigintas a string) and, if no content type was given, sent asapplication/json
Responses: JSON content types are parsed, text/* is read as text, 204/205/304 and
empty bodies yield {}, and everything else is exposed as a blob. Non-2xx statuses other
than 304 throw an ApiError.
Kubb codegen integration
Consuming apps generate typed API code using createKubbConfig() from @construkt-kit/config/kubb. The config produces 3 output directories from an OpenAPI spec:
| Output dir | Contents |
| ---------- | ----------------------------------------------- |
| dtos/ | TypeScript types generated from OpenAPI schemas |
| calls/ | API call functions (typed fetch wrappers) |
| hooks/ | React Query hooks grouped by API path |
How it connects to createApiClient:
- App creates a client:
const client = createApiClient(() => authToken) - App calls
setApiConfig({ baseURL: "https://api.example.com" }) - App re-exports the configured client from a known path (default:
@/api/client) - Kubb
clientImportPathoption points generatedcalls/to that re-export - Generated
hooks/import fromcalls/, which use the configured client
Query keys in generated hooks are prefixed with "v5" — bump this in @construkt-kit/config/kubb when making breaking API changes to invalidate all caches.
Key Kubb options (via createKubbConfig()):
inputPath— OpenAPI spec location (default:./src/api/openapi.json)outputPath— generated output root (default:./src/api/gen)clientImportPath— where generated code imports the client from (default:@/api/client)
CLI: construkt-kit-api-gen
The package ships a construkt-kit-api-gen binary that automates the full codegen workflow: fetch an OpenAPI spec from a running API, run Kubb codegen, and clean up.
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.tsConfig file (api.config.ts)
import { createKubbConfig } from "@construkt-kit/config/kubb";
export const specUrl = "https://api.example.com";
export default createKubbConfig({ clientImportPath: "@/api/client" });URL resolution priority
--urlCLI flagAPI_URLenvironment variablespecUrlnamed export from config file
The spec is fetched from {baseUrl}/openapi/v1.json, saved temporarily, passed to Kubb, then deleted.
