npm package: @simplix-react/api
Description: Orval mutator singleton for simplix-react domain packages
Latest version: 0.2.2

<p align="center">
  <img src="../../docs/assets/simplix-logo.png" alt="simplix-react" width="200" />
</p>

# @simplix-react/api

Orval mutator singleton for simplix-react domain packages. Provides a centralized HTTP client registry, a typed `HttpError` class, and error classification utilities for consistent error handling across generated domain packages.

## Installation

```bash
pnpm add @simplix-react/api
```

> **Note:** Domain packages generated by `simplix openapi` include this dependency automatically.

## API Overview

| Export | Kind | Description |
| --- | --- | --- |
| `configureMutator` | Function | Register an `OrvalMutator` fetch function (default or named strategy) |
| `getMutator` | Function | Retrieve a registered mutator by strategy name |
| `setRequestLocale` | Function | Set locale for automatic `Accept-Language` header injection |
| `getRequestLocale` | Function | Get the currently configured request locale |
| `HttpError` | Class | Typed HTTP error with `status`, `message`, and optional `data` |
| `getValidationErrors` | Function | Extract validation field errors from any error shape |
| `getErrorMessage` | Function | Extract a human-readable message from any error |
| `getErrorCode` | Function | Extract an application error code from any error |
| `getHttpStatus` | Function | Extract HTTP status code from any error |
| `classifyError` | Function | Classify an error into a category (`ServerErrorEvent`) |
| `createMutationErrorHandler` | Function | Create a React Query `onError` handler with per-category callbacks |

### Type Exports

| Export | Description |
| --- | --- |
| `OrvalMutator` | Mutator function signature: `(url: string, options?: RequestInit) => Promise<T>` |
| `ErrorType` | Orval error type alias: `Error & { data?: T }` |
| `BodyType` | Orval body type alias: `T` |
| `ValidationFieldError` | Field-level validation error (`field`, `message`) |
| `ErrorCategory` | Error category union: `"validation" \| "auth" \| "client" \| "server" \| "network" \| "unknown"` |
| `ServerErrorEvent` | Classified error event with category, status, errorCode, message, and validationErrors |

## Key Concepts

### HttpError

A typed error class for HTTP responses with non-2xx status codes. Carries the HTTP `status`, error `message`, and optional response `data`.

```ts
import { HttpError } from "@simplix-react/api";

async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
  const res = await fetch(url, options);
  if (!res.ok) {
    const data = await res.json().catch(() => undefined);
    throw new HttpError(res.status, res.statusText, data);
  }
  return res.json() as Promise<T>;
}
```

### configureMutator / getMutator

A global registry for Orval mutator functions. Domain packages call `getMutator()` in their generated client code. The application configures the mutator once at startup.

```ts
import { configureMutator, getMutator } from "@simplix-react/api";

// Register the default mutator
configureMutator(async (url, options) => {
  const res = await fetch(url, options);
  if (!res.ok) throw new HttpError(res.status, res.statusText);
  return res.json();
});

// Register a named strategy (for multi-backend setups)
configureMutator("admin-api", async (url, options) => {
  const res = await fetch(`/admin${url}`, options);
  if (!res.ok) throw new HttpError(res.status, res.statusText);
  return res.json();
});

// Retrieve (used internally by generated code)
const mutator = getMutator();            // default strategy
const adminMutator = getMutator("admin-api"); // named strategy
```

### Request Locale (Accept-Language Header)

When a locale is configured via `setRequestLocale`, `getMutator` automatically injects an `Accept-Language` header into every outgoing request. This ensures the server receives the application's selected language rather than the browser's OS-level language preference.

```ts
import { setRequestLocale, getRequestLocale } from "@simplix-react/api";

// Set the locale (typically wired to i18n adapter)
setRequestLocale("en");

// All subsequent API requests include: Accept-Language: en
const mutator = getMutator();
await mutator("/api/pets"); // → Accept-Language: en

// Check current locale
getRequestLocale(); // "en"
```

#### How It Works

1. `setRequestLocale(locale)` stores the locale in a module-scoped variable (tab-local, not shared across tabs)
2. `getMutator()` returns a wrapper that merges `Accept-Language` into the request headers as a plain object
3. If the caller already sets an explicit `Accept-Language` header, it is **not** overwritten
4. If no locale is configured, `getMutator()` returns the raw mutator without modification

#### Why This Is Needed

Browsers set the `Accept-Language` header automatically on all HTTP requests, but the value reflects **OS/browser language settings**, not the application's i18n selection. For example, a user with a Korean OS who selects English in the app would still send `Accept-Language: ko` without this feature. `setRequestLocale` ensures the server receives the correct language.

#### Integration with @simplix-react/i18n

Wire the i18n adapter's locale changes to `setRequestLocale` in the app's i18n config:

```ts
import { setRequestLocale } from "@simplix-react/api";
import { createI18nConfig } from "@simplix-react/i18n";

export const { adapter: i18nAdapter, i18nReady } = createI18nConfig({ ... });

// Sync locale changes to API request headers
i18nAdapter.onLocaleChange(setRequestLocale);
i18nReady.then(() => setRequestLocale(i18nAdapter.locale));
```

After this setup, every API request automatically includes `Accept-Language` matching the app's active locale. When the user switches language, subsequent requests reflect the new locale immediately.

> **Note:** `@simplix-react/api` and `@simplix-react/i18n` have no direct dependency on each other. The wiring happens at the application level, keeping both packages decoupled.

### Error Utilities

A set of duck-typing utilities that extract structured information from any error shape. They work with `HttpError`, Orval's `ApiError`, Spring Boot error responses, Rails-style errors, and plain objects.

#### getValidationErrors

Extracts field-level validation errors from any error. Supports multiple shapes:

- `{ errorDetail: [{ field, message }] }` (Spring Boot / ApiResponseError)
- `{ errors: [{ field, message }] }` (plain array)
- `{ errors: { [field]: string[] } }` (Rails-style)
- `HttpError.data` containing any of the above
- `JSON.parse(error.body)` fallback

```ts
import { getValidationErrors } from "@simplix-react/api";

const errors = getValidationErrors(error);
if (errors) {
  errors.forEach(({ field, message }) => {
    form.setFieldError(field, message);
  });
}
```

#### getErrorMessage / getErrorCode / getHttpStatus

```ts
import { getErrorMessage, getErrorCode, getHttpStatus } from "@simplix-react/api";

const message = getErrorMessage(error);  // "Not Found" or "An unknown error occurred"
const code = getErrorCode(error);        // "USER_NOT_FOUND" or null
const status = getHttpStatus(error);     // 404 or null
```

#### classifyError

Classifies any error into a `ServerErrorEvent` with a category:

| Category | Condition |
| --- | --- |
| `network` | `TypeError` with no HTTP status |
| `auth` | Status 401 or 403 |
| `server` | Status >= 500 |
| `validation` | Status 400-499 with validation errors |
| `client` | Status 400-499 without validation errors |
| `unknown` | Everything else |

```ts
import { classifyError } from "@simplix-react/api";

const event = classifyError(error);
// event.category  → "validation"
// event.status    → 422
// event.message   → "Validation failed"
// event.validationErrors → [{ field: "email", message: "is required" }]
```

#### createMutationErrorHandler

Creates a React Query `onError` callback with per-category handlers. Useful with `useMutation`'s `onError` option.

```ts
import { createMutationErrorHandler } from "@simplix-react/api";

const onError = createMutationErrorHandler({
  onValidationError: (event) => {
    event.validationErrors?.forEach(({ field, message }) => {
      form.setFieldError(field, message);
    });
  },
  onAuthError: () => {
    redirect("/login");
  },
  onServerError: (event) => {
    toast.error(event.message);
  },
  onNetworkError: () => {
    toast.error("Network error. Please check your connection.");
  },
  onUnknownError: (event) => {
    toast.error(event.message);
  },
});

// Use with React Query
useMutation({
  mutationFn: createUser,
  onError,
});
```

## Related Packages

| Package | Description |
| --- | --- |
| `@simplix-react/contract` | Zod-based type-safe API contract definitions |
| `@simplix-react/react` | React Query hooks derived from contracts |
| `@simplix-react/auth` | Authentication middleware (Bearer, API Key, OAuth2) |

---

Next Step -> [`@simplix-react/auth`](../auth/README.md)