Skip to content

API Client

@granit/api-client is the HTTP foundation for all Granit frontend packages. It creates pre-configured Axios instances with automatic Bearer token injection, optional X-Tenant-Id header for multi-tenant applications, and a 401 response interceptor for back-channel logout. @granit/idempotency extends it with automatic Idempotency-Key header injection on mutation requests, pairing with Granit.Idempotency on the .NET backend.

Peer dependencies: axios ^1.0.0

  • Directory@granit/api-client/ Axios factory, global setters, error classes, RFC 7807 types
    • @granit/idempotency Automatic Idempotency-Key header injection
PackageRoleDepends on
@granit/api-clientcreateApiClient(), createMutator(), global interceptor wiringaxios
@granit/idempotencyenableIdempotency() for mutation request deduplication@granit/api-client, axios
import { createApiClient } from '@granit/api-client';
export const api = createApiClient({
baseURL: import.meta.env.VITE_API_URL,
timeout: 15_000, // optional, default: 10_000 ms
});

Creates an Axios instance with request interceptors for Bearer token and tenant header, plus a response interceptor for 401 handling.

interface ApiClientConfig {
baseURL: string;
timeout?: number; // default: 10_000 ms
}
function createApiClient(config: ApiClientConfig): AxiosInstance;

The instance automatically:

  • Injects Authorization: Bearer <token> if a token getter is registered
  • Injects X-Tenant-Id: <id> if a tenant getter is registered
  • Calls the onUnauthorized callback on HTTP 401, then re-throws the error

Creates an orval-compatible mutator from an existing Axios instance. The mutator reuses all interceptors and returns response.data directly.

function createMutator(
instance: AxiosInstance
): <T>(config: AxiosRequestConfig, options?: AxiosRequestConfig) => Promise<T>;

These functions wire global behavior into every Axios instance created by createApiClient. Most are called automatically by companion packages — manual use is only needed when those packages are not installed.

FunctionCalled automatically byPurpose
setTokenGetter(getter)@granit/react-authenticationAsync Keycloak token getter
setTenantGetter(getter)@granit/react-multi-tenancySync tenant ID for X-Tenant-Id
setOnUnauthorized(callback)@granit/react-authenticationHTTP 401 handler (back-channel logout)
setIdempotencyKeyGenerator(generator)@granit/idempotencyMutation request key generator
function setTokenGetter(getter: () => Promise<string | undefined>): void;
function setTenantGetter(getter: () => string | undefined): void;
function setOnUnauthorized(callback: () => void): void;
function setIdempotencyKeyGenerator(
generator: (config: InternalAxiosRequestConfig) => string | undefined
): void;

All error classes are structured for programmatic handling. They pair with the RFC 7807 Problem Details returned by the Granit .NET backend.

Thrown when the backend returns a non-2xx response with a ProblemDetails body.

class HttpError extends Error {
readonly name = 'HttpError';
readonly status: number;
readonly problemDetails?: ProblemDetailsPayload;
}

Thrown for HTTP 422 responses with field-level validation errors.

class ValidationError extends Error {
readonly name = 'ValidationError';
readonly details?: ValidationDetails;
}
interface ValidationDetails {
readonly field?: string;
readonly constraint?: string;
readonly fieldErrors?: Readonly<Record<string, readonly string[]>>;
}

Thrown when a request exceeds the configured timeout.

class TimeoutError extends Error {
readonly name = 'TimeoutError';
readonly timeoutMs: number;
}
// RFC 7807 Problem Details — mirrors Granit.ExceptionHandling output
interface ProblemDetails {
type?: string;
title: string;
status: number;
detail?: string;
instance?: string;
traceId?: string; // OpenTelemetry trace ID for Grafana correlation
errorCode?: string; // Domain error code, e.g. "Appointment:SlotUnavailable"
}
// Paginated list — mirrors Granit.Querying PagedResult<T>
interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
}

@granit/idempotency injects a unique Idempotency-Key header on every mutation request (POST, PUT, PATCH, DELETE). The .NET backend (Granit.Idempotency) uses this key to deduplicate requests — same key + same payload returns the cached response, same key + different payload returns HTTP 409.

interface IdempotencyOptions {
methods?: string[]; // default: ['post', 'put', 'patch', 'delete']
headerName?: string; // default: 'Idempotency-Key'
keyGenerator?: (config: InternalAxiosRequestConfig) => string | undefined;
}
function enableIdempotency(options?: IdempotencyOptions): void;
function disableIdempotency(): void;

The default key generator uses crypto.randomUUID(). Call enableIdempotency() once in main.ts — it registers the generator via setIdempotencyKeyGenerator() on the shared @granit/api-client instance.

Import from @granit/api-client/test-utils for Vitest:

import { createMockClient, axiosResponse } from '@granit/api-client/test-utils';
const client = createMockClient();
vi.mocked(client.get).mockResolvedValue(axiosResponse({ items: [], total: 0 }));
FunctionPurpose
createMockClient()Fully mocked AxiosInstance with vi.fn() stubs
axiosResponse(data)Wraps data in a minimal AxiosResponse<T> shape
CategoryKey exportsPackage
FactorycreateApiClient(), createMutator()@granit/api-client
Global setterssetTokenGetter(), setTenantGetter(), setOnUnauthorized(), setIdempotencyKeyGenerator()@granit/api-client
Error classesHttpError, ValidationError, TimeoutError@granit/api-client
Response typesProblemDetails, ProblemDetailsPayload, ValidationDetails, PaginatedResponse<T>, ApiClientConfig@granit/api-client
IdempotencyenableIdempotency(), disableIdempotency(), IdempotencyOptions@granit/idempotency
Test utilitiescreateMockClient(), axiosResponse()@granit/api-client/test-utils