---
title: "Extend the client"
description: "The generated package is replaced whole on every regeneration. Put your configuration, logging, and domain logic in modules you own that wrap the client."
url: https://typeship.dev/docs/guides/customize
markdown: https://typeship.dev/docs/guides/customize.md
section: "Get started"
---
> ## Documentation index
> Fetch the complete documentation index at https://typeship.dev/llms.txt (every page, one line each) or the full text at https://typeship.dev/llms-full.txt.
> Append .md to any docs URL, or send Accept: text/markdown, for the markdown twin of that page.

# Extend the client

The generated package is replaced whole on every regeneration. Put your configuration, logging, and domain logic in modules you own that wrap the client.

The generated package is wholly owned by the generator. Regeneration replaces the directory, and anything you wrote inside it is gone. That is a feature. It is what makes the next pull request a clean diff of spec changes and nothing else.

Customization lives in code you own that wraps the client. The client was designed to make that easy.

## A factory you own

Pin the base URL, auth, and defaults in one place so call sites never repeat configuration:

```ts title="src/lib/acme.ts (your code, outside the generated package)"
import { AcmeClient } from "acme";

export function createAcmeClient(): AcmeClient {
  return new AcmeClient({
    baseUrl: process.env.ACME_BASE_URL,
    bearerToken: process.env.ACME_TOKEN!,
    defaultHeaders: { "Request-Source": "core-team" },
  });
}
```

## Instrument with hooks and `fetch`

Logging, metrics, tracing, and proxies belong in the `fetch` option or the hooks, not in generated files:

```ts
const client = new AcmeClient({
  bearerToken: token,
  fetch: async (input, init) => {
    const started = Date.now();
    const response = await fetch(input, init);
    console.log(`${init?.method ?? "GET"} ${String(input)} ${response.status} ${Date.now() - started}ms`);
    return response;
  },
  onRequest: (context) => {
    context.headers["Trace-Id"] = crypto.randomUUID();
  },
});
```

Python takes `transport=`, `on_request=`, and friends. Go takes `WithHTTPClient` and `WithOnRequest`. See [Hooks and debug logging](https://typeship.dev/docs/platforms/sdk#hooks-and-debug-logging).

## Domain logic on top

Caching, mapping to your own types, and business rules belong in your functions that call the client and pass results on:

```ts
import { unwrap } from "acme";
import { createAcmeClient } from "./acme";

const client = createAcmeClient();

export async function activeAccountIds(): Promise<string[]> {
  const ids: string[] = [];
  for await (const account of client.accounts.list({ limit: 100 })) {
    if (account.status === "active") ids.push(account.id);
  }
  return ids;
}
```

## Things that belong in the project, not in code

Some customizations change what gets generated. Those live in the project's [config](https://typeship.dev/docs/projects/config) so every regeneration carries them:

* Parameters every call should carry: [global parameters](https://typeship.dev/docs/projects/config#global-parameters).
* Which statuses to retry and how: [retry tuning](https://typeship.dev/docs/projects/config#retry-tuning).
* How an unusual endpoint pages: [pagination rules](https://typeship.dev/docs/projects/config#pagination-rules).
* A wrong type or name in a spec you cannot edit: [spec patches](https://typeship.dev/docs/projects/spec-patches).

With this split, regeneration never destroys your code. The package directory is disposable, and everything you wrote lives outside it.

## Sitemap

[Every page of these docs](https://typeship.dev/llms.txt)
