> ## Documentation Index
> Fetch the complete documentation index at: https://komga-sdk.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Best Practices

> Patterns for maintainable Komga integrations with the generated client

`komga-sdk` keeps transport and generated API calls small. Put application policy around the client rather than relying on implicit middleware.

## Reuse one configured client

Create a client at your application boundary and pass it to operations:

```typescript title="lib/komga.ts" theme={null}
import { createKomgaClient } from 'komga-sdk';

export const client = createKomgaClient({
  baseUrl: process.env.KOMGA_URL!,
  auth: {
    type: 'apiKey',
    key: process.env.KOMGA_API_KEY!,
  },
});
```

```typescript title="books.ts" theme={null}
import { getBookById } from 'komga-sdk';
import { client } from './lib/komga.js';

export async function loadBook(bookId: string) {
  return getBookById({ bookId }, { client });
}
```

A shared client keeps the base URL, authentication, headers, credentials mode, and Fetch implementation consistent. Do not create a new client for every request unless the request genuinely needs a different server, account, or transport policy. Separate clients are useful for public and private servers, or for a browser session and a server-side credential.

## Keep credentials out of source

Use environment variables or a secrets manager:

```dotenv title=".env" theme={null}
KOMGA_URL=https://komga.example.com
KOMGA_API_KEY=replace-me
```

Use HTTPS for any non-local connection. In browser applications, keep Basic credentials and API keys on a server-side boundary when they must remain private. Redact `Authorization`, `X-API-Key`, cookies, and `Set-Cookie` before logging requests or responses.

## Choose a result boundary

Generated functions are nonthrowing by default. Handle the result locally when a failed request is an expected branch:

```typescript theme={null}
import { client } from './lib/komga.js';
import { getBookById } from 'komga-sdk';

export async function findBook(bookId: string) {
  const result = await getBookById({ bookId }, { client });

  if (result.response?.status === 404) {
    return null;
  }

  if (result.error) {
    console.error('Komga request failed', result.response?.status, result.error);
    throw new Error('Unable to load book');
  }

  return result.data;
}
```

Use `unwrap` when your application boundary is exception-based. It throws `KomgaApiError` for an error result and preserves the response status and body:

```typescript theme={null}
import { client } from './lib/komga.js';
import { KomgaApiError, getBookById, unwrap } from 'komga-sdk';

export async function requireBook(bookId: string) {
  try {
    return unwrap(await getBookById({ bookId }, { client }));
  } catch (error) {
    if (error instanceof KomgaApiError && error.status === 404) {
      return null;
    }
    throw error;
  }
}
```

The core client does not throw, validate, or transform errors unless you call `unwrap`, set generated `throwOnError`, or add an application policy yourself. Keep one policy at each boundary instead of mixing result checks and broad `try`/`catch` blocks in every caller.

## Paginate deliberately

Use a page size that fits the operation and memory budget. Keep the required `bookSearch` field in every `getBooks` call:

```typescript theme={null}
import { client } from './lib/komga.js';
import { getBooks } from 'komga-sdk';

export async function collectBookTitles() {
  const titles: string[] = [];

  for (let page = 0; ; page += 1) {
    const result = await getBooks(
      {
        page,
        size: 100,
        sort: ['metadata.title,asc'],
        bookSearch: {},
      },
      { client },
    );

    if (result.error) {
      throw new Error(`Book page failed: ${result.response?.status ?? 'unknown'}`);
    }

    for (const book of result.data.content ?? []) {
      titles.push(book.metadata.title);
    }

    if (result.data.last ?? true) {
      break;
    }
  }

  return titles;
}
```

Use `unpaged: true` only for bounded datasets. For large batch jobs, process one page at a time, use a stable sort, and add a delay or concurrency limit in your application when the server needs it. Avoid changing filters or sort order during a multi-page export; doing so can duplicate or skip records.

## Make batch work observable and safe

Mutations can return `204 No Content`, so success is indicated by `error === undefined`, not by a value in `data`:

```typescript theme={null}
import { updateBookMetadata } from 'komga-sdk';
import { client } from './lib/komga.js';

export async function renameBook(bookId: string, title: string) {
  const result = await updateBookMetadata(
    { bookId, bookMetadataUpdateDto: { title } },
    { client },
  );

  if (result.error) {
    throw new Error(`Update failed: ${result.response?.status ?? 'unknown'}`);
  }
}
```

Only retry operations that are safe to repeat. Reads are usually easier to retry than mutations; for a mutation, use an idempotent server operation or an application request identifier when available. Respect `Retry-After` for rate limits and cap exponential backoff rather than issuing an unbounded burst.

A small application-owned wrapper can handle transient result failures without pretending the SDK has a retry policy:

```typescript theme={null}
async function withBackoff<T>(
  operation: () => Promise<{ data?: T; error?: unknown; response?: Response }>,
  attempts = 3,
): Promise<T> {
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    const result = await operation();
    if (result.error === undefined) {
      return result.data as T;
    }

    const status = result.response?.status;
    const retryable = status === undefined || status === 408 || status === 429 || status >= 500;
    if (!retryable || attempt === attempts) {
      throw result.error;
    }

    const retryAfter = Number(result.response?.headers.get('retry-after'));
    const delay = Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** (attempt - 1);
    await new Promise((resolve) => setTimeout(resolve, delay));
  }

  throw new Error('Retry policy exhausted');
}
```

Treat the wrapper as application code: classify operations, log attempts without secrets, and add cancellation when a job or request is abandoned.

## Add policies outside the SDK

The core client uses native Fetch and has no retries, caching, timeout policy, or debug logger. If your application needs a policy, make it visible at the boundary:

```typescript theme={null}
import { createKomgaClient } from 'komga-sdk';

const fetchWithTracing: typeof fetch = async (input, init) => {
  const started = Date.now();
  try {
    return await fetch(input, init);
  } finally {
    console.log(`Komga request completed in ${Date.now() - started}ms`);
  }
};

const client = createKomgaClient({
  baseUrl: process.env.KOMGA_URL!,
  fetch: fetchWithTracing,
});
```

For per-request cancellation, pass a standard `AbortSignal` through generated options:

```typescript theme={null}
const result = await getBookById(
  { bookId: 'book-123' },
  { client, signal: AbortSignal.timeout(10_000) },
);
```

Keep cache invalidation and timeout decisions with the application that owns the data lifecycle. A cached library list may be useful; book progress and mutation results usually need explicit invalidation.

## Cache only stable, bounded data

Caching belongs to the application, not the generated client:

```typescript theme={null}
import { getLibraries } from 'komga-sdk';
import { client } from './lib/komga.js';

let cached: { value: Awaited<ReturnType<typeof getLibraries>>; at: number } | undefined;

export async function loadLibraries() {
  if (cached && Date.now() - cached.at < 5 * 60_000) {
    return cached.value;
  }

  const value = await getLibraries({ client });
  cached = { value, at: Date.now() };
  return value;
}
```

Cache the result contract rather than assuming a successful response. Invalidate after library mutations, and do not cache private data across users or authentication contexts.

## Treat generated types as compile-time types

Generated types describe the API contract to TypeScript. They do not validate data at runtime:

```typescript theme={null}
import type { BookDto } from 'komga-sdk';

function bookLabel(book: BookDto): string {
  return `${book.metadata.title} #${book.number}`;
}
```

If an application needs runtime validation, add an app-owned validator at its boundary and decide how validation failures should be reported. The core package does not include one. Avoid `any`; use the generated DTO, page, search, response, and error types or narrow `unknown` at an explicit boundary.

## Respect permissions and the runtime

Generated operations carry Komga's operation documentation, including required roles such as `ADMIN` or `FILE_DOWNLOAD`. A `403` is a permission result, not a reason to retry unchanged credentials. Check the account's library sharing and operation role before scheduling a batch job.

The SDK requires a runtime with Fetch, `Headers`, `Request`, `Response`, `TextEncoder`, and Web Crypto support. Node.js 18+, modern browsers, and compatible edge runtimes provide these APIs. Pass `fetch` explicitly when the host runtime supplies a custom implementation.

## Keep operation calls explicit

Prefer a direct generated operation with its flat parameter object:

```typescript theme={null}
import { client } from './lib/komga.js';
import { getLibraries } from 'komga-sdk';

const result = await getLibraries({ client });
```

This keeps the endpoint, parameters, permission boundary, and result handling visible to the next maintainer. See [Quick reference](/quick-reference) for more operation patterns.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.