> ## Documentation Index
> Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Clients

> Generate typed API clients in any language from your Contract services

# Client Generation

One of the best things about Contract is that you can generate typed clients in any language. Your Go service becomes a TypeScript client, Python client, or any other language - with full type safety.

<Info>
  **Native SDK Generation**: Mizu now includes built-in SDK generators for Go, Python, and TypeScript that produce idiomatic, zero-dependency clients with better developer experience than OpenAPI-based generation. See [SDK Generation](/contract/sdk-overview) for details.
</Info>

## Two Approaches to Client Generation

| Approach                                  | Best For                                 | Pros                                                    | Cons                            |
| ----------------------------------------- | ---------------------------------------- | ------------------------------------------------------- | ------------------------------- |
| **[Native SDKs](/contract/sdk-overview)** | Go, Python, TypeScript                   | Idiomatic code, zero deps, better DX, streaming support | Limited language support        |
| **OpenAPI (this page)**                   | Other languages, ecosystem compatibility | 40+ languages, mature tooling                           | Generic code, more dependencies |

<CardGroup cols={2}>
  <Card title="Native SDK Generation" icon="rocket" href="/contract/sdk-overview">
    Built-in generators for Go, Python, and TypeScript with OpenAI-style developer experience
  </Card>

  <Card title="OpenAPI Generation" icon="file-code">
    Use OpenAPI spec with third-party generators for any language (continue reading below)
  </Card>
</CardGroup>

## How It Works

Contract provides machine-readable descriptions of your API:

```
Your Go Service
     ↓
OpenAPI Spec / MCP Tools
     ↓
Client Generator (openapi-generator, etc.)
     ↓
TypeScript Client, Python Client, Go Client, etc.
```

## Two Ways to Get API Metadata

| Endpoint            | Format      | Best For                     |
| ------------------- | ----------- | ---------------------------- |
| `/openapi.json`     | OpenAPI 3.1 | Most languages, best tooling |
| `/mcp` (tools/list) | MCP tools   | AI assistants                |

## OpenAPI Client Generation

OpenAPI has the most mature tooling. This is the recommended approach.

### Step 1: Serve OpenAPI Spec

```go theme={null}
import (
    "github.com/go-mizu/mizu"
    contract "github.com/go-mizu/mizu/contract/v2"
    "github.com/go-mizu/mizu/contract/v2/transport/rest"
    "github.com/go-mizu/mizu/contract/v2/transport/openapi"

    "yourapp/todo"
)

impl := todo.NewService()
svc := contract.Register[todo.API](impl,
    contract.WithDefaultResource("todos"),
)

app := mizu.New()
rest.Mount(app.Router, svc)
openapi.Mount(app.Router, "/openapi.json", svc)

app.Listen(":8080")
```

### Step 2: Download the Spec

```bash theme={null}
curl http://localhost:8080/openapi.json > openapi.json
```

### Step 3: Generate Clients

Use any OpenAPI code generator. Here are the most popular:

#### TypeScript

Using `openapi-typescript` (types only):

```bash theme={null}
npm install -D openapi-typescript
npx openapi-typescript openapi.json -o ./src/api/types.ts
```

Using `openapi-typescript-codegen` (full client):

```bash theme={null}
npm install -D openapi-typescript-codegen
npx openapi-typescript-codegen --input openapi.json --output ./src/api
```

Using `openapi-generator`:

```bash theme={null}
npm install -g @openapitools/openapi-generator-cli
openapi-generator-cli generate -i openapi.json -g typescript-fetch -o ./api-client
```

#### Python

Using `openapi-python-client`:

```bash theme={null}
pip install openapi-python-client
openapi-python-client generate --path openapi.json --output-path ./client
```

Using `openapi-generator`:

```bash theme={null}
openapi-generator-cli generate -i openapi.json -g python -o ./python-client
```

#### Go

Using `oapi-codegen`:

```bash theme={null}
go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@latest
oapi-codegen -package client openapi.json > client/client.go
```

Using `openapi-generator`:

```bash theme={null}
openapi-generator-cli generate -i openapi.json -g go -o ./go-client
```

#### Other Languages

```bash theme={null}
# Rust
openapi-generator-cli generate -i openapi.json -g rust -o ./rust-client

# Java
openapi-generator-cli generate -i openapi.json -g java -o ./java-client

# Ruby
openapi-generator-cli generate -i openapi.json -g ruby -o ./ruby-client

# PHP
openapi-generator-cli generate -i openapi.json -g php -o ./php-client
```

## Handwritten Clients

Sometimes you don't need code generation - a simple handwritten client works fine.

### Go Client

```go theme={null}
package todoclient

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
)

type Client struct {
    baseURL    string
    httpClient *http.Client
}

func New(baseURL string) *Client {
    return &Client{
        baseURL:    baseURL,
        httpClient: &http.Client{},
    }
}

// Types
type Todo struct {
    ID    string `json:"id"`
    Title string `json:"title"`
    Done  bool   `json:"done"`
}

type CreateInput struct {
    Title string `json:"title"`
}

type ListOutput struct {
    Items []*Todo `json:"items"`
    Count int     `json:"count"`
}

// Methods

func (c *Client) Create(ctx context.Context, input *CreateInput) (*Todo, error) {
    return doRequest[Todo](ctx, c, "POST", "/todos", input)
}

func (c *Client) List(ctx context.Context) (*ListOutput, error) {
    return doRequest[ListOutput](ctx, c, "GET", "/todos", nil)
}

func (c *Client) Get(ctx context.Context, id string) (*Todo, error) {
    return doRequest[Todo](ctx, c, "GET", "/todos/"+id, nil)
}

func (c *Client) Delete(ctx context.Context, id string) error {
    _, err := doRequest[struct{}](ctx, c, "DELETE", "/todos/"+id, nil)
    return err
}

// Helper
func doRequest[T any](ctx context.Context, c *Client, method, path string, body any) (*T, error) {
    var bodyReader *bytes.Reader
    if body != nil {
        data, _ := json.Marshal(body)
        bodyReader = bytes.NewReader(data)
    }

    req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Content-Type", "application/json")

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 400 {
        return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
    }

    var result T
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, err
    }
    return &result, nil
}
```

Usage:

```go theme={null}
client := todoclient.New("http://localhost:8080")

todo, err := client.Create(ctx, &todoclient.CreateInput{Title: "Test"})
if err != nil {
    log.Fatal(err)
}
fmt.Println(todo.ID)
```

### Python Client

```python theme={null}
from dataclasses import dataclass
from typing import List, Optional
import requests

@dataclass
class Todo:
    id: str
    title: str
    done: bool

@dataclass
class ListOutput:
    items: List[Todo]
    count: int

class TodoClient:
    def __init__(self, base_url: str):
        self.base_url = base_url.rstrip('/')

    def create(self, title: str) -> Todo:
        response = requests.post(
            f"{self.base_url}/todos",
            json={"title": title}
        )
        response.raise_for_status()
        data = response.json()
        return Todo(**data)

    def list(self) -> ListOutput:
        response = requests.get(f"{self.base_url}/todos")
        response.raise_for_status()
        data = response.json()
        return ListOutput(
            items=[Todo(**item) for item in data["items"]],
            count=data["count"]
        )

    def get(self, id: str) -> Optional[Todo]:
        response = requests.get(f"{self.base_url}/todos/{id}")
        if response.status_code == 404:
            return None
        response.raise_for_status()
        return Todo(**response.json())

    def delete(self, id: str) -> None:
        response = requests.delete(f"{self.base_url}/todos/{id}")
        response.raise_for_status()
```

Usage:

```python theme={null}
client = TodoClient("http://localhost:8080")

# Create
todo = client.create("Buy milk")
print(f"Created: {todo.id}")

# List
result = client.list()
print(f"Total: {result.count}")

# Get
todo = client.get("1")
if todo:
    print(f"Found: {todo.title}")
```

## JSON-RPC Client

For JSON-RPC transport, clients need to handle the JSON-RPC envelope:

### TypeScript

```typescript theme={null}
interface JsonRpcRequest {
  jsonrpc: '2.0';
  id: number;
  method: string;
  params?: any;
}

interface JsonRpcResponse<T> {
  jsonrpc: '2.0';
  id: number;
  result?: T;
  error?: { code: number; message: string };
}

class JsonRpcClient {
  private nextId = 1;

  constructor(private baseUrl: string) {}

  async call<T>(method: string, params?: any): Promise<T> {
    const request: JsonRpcRequest = {
      jsonrpc: '2.0',
      id: this.nextId++,
      method,
      params,
    };

    const response = await fetch(this.baseUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(request),
    });

    const rpcResponse: JsonRpcResponse<T> = await response.json();

    if (rpcResponse.error) {
      throw new Error(`${rpcResponse.error.code}: ${rpcResponse.error.message}`);
    }

    return rpcResponse.result!;
  }

  // Batch multiple calls in one request
  async batch<T extends any[]>(calls: { method: string; params?: any }[]): Promise<T> {
    const requests = calls.map((call, i) => ({
      jsonrpc: '2.0' as const,
      id: this.nextId++,
      method: call.method,
      params: call.params,
    }));

    const response = await fetch(this.baseUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requests),
    });

    const responses: JsonRpcResponse<any>[] = await response.json();
    return responses.map((r) => r.result) as T;
  }
}

// Typed wrapper
class TodoJsonRpcClient {
  private rpc: JsonRpcClient;

  constructor(baseUrl: string) {
    this.rpc = new JsonRpcClient(baseUrl + '/rpc');
  }

  create(input: CreateInput): Promise<Todo> {
    return this.rpc.call('Create', input);
  }

  list(): Promise<ListOutput> {
    return this.rpc.call('List');
  }

  // Batch create multiple todos in one request
  async createMany(inputs: CreateInput[]): Promise<Todo[]> {
    return this.rpc.batch(inputs.map(input => ({ method: 'Create', params: input })));
  }
}
```

## Error Handling in Clients

Always handle errors properly in your clients:

### TypeScript

```typescript theme={null}
class ApiError extends Error {
  constructor(
    message: string,
    public code: string,
    public status: number
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

async function handleResponse<T>(response: Response): Promise<T> {
  if (!response.ok) {
    const body = await response.text();
    throw new ApiError(body, 'HTTP_ERROR', response.status);
  }

  const data = await response.json();
  return data;
}

// In client methods
async create(input: CreateInput): Promise<Todo> {
  try {
    const response = await fetch(`${this.baseUrl}/todos`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(input),
    });
    return handleResponse<Todo>(response);
  } catch (error) {
    if (error instanceof ApiError) {
      // Handle specific error codes
      switch (error.status) {
        case 400:
          throw new Error('Invalid input: ' + error.message);
        case 404:
          throw new Error('Not found');
        default:
          throw error;
      }
    }
    throw error;
  }
}
```

### Go

```go theme={null}
type APIError struct {
    Code    string
    Message string
    Status  int
}

func (e *APIError) Error() string {
    return fmt.Sprintf("[%d] %s: %s", e.Status, e.Code, e.Message)
}

func doRequest[T any](ctx context.Context, c *Client, method, path string, body any) (*T, error) {
    // ... (make request)

    if resp.StatusCode >= 400 {
        body, _ := io.ReadAll(resp.Body)
        return nil, &APIError{
            Code:    "HTTP_ERROR",
            Message: string(body),
            Status:  resp.StatusCode,
        }
    }

    // ... (decode response)
}

// Usage
todo, err := client.Get(ctx, "123")
if err != nil {
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        if apiErr.Status == 404 {
            // Handle not found
        }
    }
}
```

## Automation with CI/CD

Automate client generation in your build:

### GitHub Action

```yaml theme={null}
name: Generate Clients

on:
  push:
    paths:
      - 'api/**'  # When API changes

jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Start API server
        run: go run main.go &
        env:
          PORT: 8080

      - name: Wait for server
        run: sleep 5

      - name: Download OpenAPI spec
        run: curl http://localhost:8080/openapi.json > openapi.json

      - name: Generate TypeScript client
        run: npx openapi-typescript openapi.json -o client/ts/types.ts

      - name: Generate Python client
        run: |
          pip install openapi-python-client
          openapi-python-client generate --path openapi.json --output-path client/python

      - name: Commit generated clients
        run: |
          git add client/
          git commit -m "Update generated clients" || true
          git push
```

## Best Practices

### 1. Version Your API

Include version in your service:

```go theme={null}
svc := contract.Register[todo.API](impl,
    contract.WithName("Todo"),
    contract.WithDefaultResource("todos"),
)
```

### 2. Keep Clients in Sync

Regenerate clients when your API changes. Consider:

* Git submodules for shared clients
* Package registry (npm, PyPI) for versioned clients
* API versioning for breaking changes

### 3. Add Custom Headers

Clients often need authentication:

```typescript theme={null}
class TodoClient {
  constructor(private baseUrl: string, private token: string) {}

  private async fetch(path: string, options: RequestInit = {}) {
    return fetch(this.baseUrl + path, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.token}`,
        ...options.headers,
      },
    });
  }
}
```

### 4. Handle Rate Limits

```typescript theme={null}
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (error instanceof ApiError && error.status === 429) {
        await sleep(Math.pow(2, i) * 1000);  // Exponential backoff
        continue;
      }
      throw error;
    }
  }
  throw new Error('Max retries exceeded');
}
```

## See Also

* [SDK Generation](/contract/sdk-overview) - Native SDK generators for Go, Python, TypeScript
* [Go SDK](/contract/sdk-go) - Native Go client generation
* [Python SDK](/contract/sdk-python) - Native Python client generation
* [TypeScript SDK](/contract/sdk-typescript) - Native TypeScript client generation
* [OpenAPI](/contract/openapi) - OpenAPI specification generation
* [REST](/contract/rest) - REST endpoints
* [JSON-RPC](/contract/jsonrpc) - JSON-RPC endpoints
* [MCP](/contract/mcp) - MCP for AI assistants
