npm package: @honorest/client
Description: Type-safe HTTP client for HonorestJS contracts - Automatically generated API client with full TypeScript support
Author: ctrhub
Latest version: 0.1.1

# @honorest/client

> Type-safe HTTP client for HonorestJS contracts

[![npm version](https://img.shields.io/npm/v/@honorest/client.svg)](https://www.npmjs.com/package/@honorest/client)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

`@honorest/client` provides an automatically generated, type-safe HTTP client for your API contracts. Built on top of [@honorest/contract](https://github.com/honorestjs/contract), it ensures complete type safety from server to client with zero code generation.

## Features

🎯 **Fully Type-Safe** - Complete TypeScript inference for inputs and outputs  
🚀 **Auto-Generated API** - Client methods generated from contracts  
🔒 **Type-Safe Errors** - Typed error handling based on contract error schemas  
⚡ **Fetch-Based** - Uses native fetch API (works in browsers and Node.js 18+)  
🎣 **Interceptors** - Request/response/error interceptors  
📦 **Lightweight** - Minimal dependencies, tree-shakeable  

## Installation

```bash
npm install @honorest/client @honorest/contract
# or
yarn add @honorest/client @honorest/contract
# or
pnpm add @honorest/client @honorest/contract
# or
bun add @honorest/client @honorest/contract
```

## Quick Start

### 1. Define Your Contract

First, create your API contract using `@honorest/contract`:

```typescript
// contracts/users.contract.ts
import { defineContract, endpoint, defineApp } from '@honorest/contract'
import { z } from 'zod'

const UsersContract = defineContract({
  name: 'users',
  path: '/users',
  endpoints: {
    getUser: endpoint({
      method: 'GET',
      path: '/:id',
      params: z.object({ id: z.string().uuid() }),
      output: z.object({
        id: z.string().uuid(),
        name: z.string(),
        email: z.string().email()
      }),
      errors: {
        404: z.object({ message: z.string() })
      }
    }),
    
    createUser: endpoint({
      method: 'POST',
      path: '/',
      body: z.object({
        name: z.string().min(1),
        email: z.string().email()
      }),
      output: z.object({
        id: z.string().uuid(),
        name: z.string(),
        email: z.string().email()
      })
    })
  }
})

export const AppContract = defineApp({
  prefix: '/api',
  contracts: {
    users: UsersContract
  }
})
```

### 2. Create the Client

```typescript
import { createClient } from '@honorest/client'
import { AppContract } from './contracts'

const api = createClient(AppContract, {
  baseUrl: 'http://localhost:3000'
})
```

### 3. Make Type-Safe API Calls

```typescript
// GET request with path parameters
const user = await api.users.getUser({ params: { id: '123' }})
//    ^? { id: string, name: string, email: string }

// POST request with body
const newUser = await api.users.createUser({
  body: { name: 'John Doe', email: 'john@example.com' }
})
//    ^? { id: string, name: string, email: string }
```

## API Reference

### `createClient(app, config)`

Creates a type-safe HTTP client from an app contract.

```typescript
const api = createClient(AppContract, {
  baseUrl: 'http://localhost:3000',
  headers: {
    'Authorization': 'Bearer token'
  },
  fetch: customFetch, // Optional custom fetch implementation
  onRequest: async (url, init) => {
    console.log('Request:', url)
  },
  onResponse: async (response) => {
    console.log('Response:', response.status)
  },
  onError: async (error) => {
    console.error('Error:', error)
  }
})
```

#### Config Options

| Option | Type | Description |
|--------|------|-------------|
| `baseUrl` | `string` | **Required**. Base URL for the API |
| `headers` | `Record<string, string>` | Default headers for all requests |
| `fetch` | `typeof fetch` | Custom fetch implementation (useful for testing) |
| `onRequest` | `(url, init) => void \| Promise<void>` | Request interceptor |
| `onResponse` | `(response) => void \| Promise<void>` | Response interceptor |
| `onError` | `(error) => void \| Promise<void>` | Error interceptor |

## Error Handling

### `ClientError`

Thrown when the API returns an error response (4xx or 5xx).

```typescript
import { ClientError } from '@honorest/client'

try {
  await api.users.getUser({ params: { id: 'invalid' }})
} catch (error) {
  if (error instanceof ClientError) {
    console.log(error.status)    // 404
    console.log(error.data)      // { message: "User not found" }
    console.log(error.message)   // "HTTP 404: Not Found"
    console.log(error.request)   // { method: 'GET', url: '...', headers: {} }
    
    // Helper methods
    if (error.is(404)) {
      console.log('Not found!')
    }
    
    if (error.isClientError()) {
      console.log('4xx error')
    }
    
    if (error.isServerError()) {
      console.log('5xx error')
    }
  }
}
```

### `NetworkError`

Thrown when the request fails before reaching the server (network issues, CORS, etc.).

```typescript
import { NetworkError } from '@honorest/client'

try {
  await api.users.getUser({ params: { id: '123' }})
} catch (error) {
  if (error instanceof NetworkError) {
    console.log('Network error:', error.message)
    console.log('Cause:', error.cause)
  }
}
```

### `ValidationError`

Thrown when response doesn't match the contract (if validation is enabled).

```typescript
import { ValidationError } from '@honorest/client'

try {
  await api.users.getUser({ params: { id: '123' }})
} catch (error) {
  if (error instanceof ValidationError) {
    console.log('Validation error:', error.message)
    console.log('Errors:', error.errors)
  }
}
```

## Advanced Usage

### Custom Headers Per Request

```typescript
const user = await api.users.getUser({
  params: { id: '123' },
  headers: {
    'X-Custom-Header': 'value'
  }
})
```

### Query Parameters

```typescript
// Endpoint definition
listUsers: endpoint({
  method: 'GET',
  path: '/',
  query: z.object({
    page: z.number().default(1),
    limit: z.number().default(20),
    search: z.string().optional()
  }),
  output: z.object({
    users: z.array(UserSchema),
    total: z.number()
  })
})

// Client usage
const result = await api.users.listUsers({
  query: {
    page: 2,
    limit: 50,
    search: 'john'
  }
})
```

### Authentication

```typescript
// Global auth token
const api = createClient(AppContract, {
  baseUrl: 'http://localhost:3000',
  headers: {
    'Authorization': `Bearer ${token}`
  }
})

// Or use request interceptor for dynamic tokens
const api = createClient(AppContract, {
  baseUrl: 'http://localhost:3000',
  onRequest: async (url, init) => {
    const token = await getAuthToken()
    init.headers = {
      ...init.headers,
      'Authorization': `Bearer ${token}`
    }
  }
})
```

### Request/Response Logging

```typescript
const api = createClient(AppContract, {
  baseUrl: 'http://localhost:3000',
  onRequest: async (url, init) => {
    console.log(`→ ${init.method} ${url}`)
  },
  onResponse: async (response) => {
    console.log(`← ${response.status}`)
  },
  onError: async (error) => {
    console.error('✗ Request failed:', error.message)
  }
})
```

### Custom Fetch Implementation

Useful for testing or adding custom behavior:

```typescript
const api = createClient(AppContract, {
  baseUrl: 'http://localhost:3000',
  fetch: async (url, init) => {
    // Custom fetch logic
    console.log('Custom fetch:', url)
    return fetch(url, init)
  }
})
```

## Type Inference

The client provides complete type inference:

```typescript
import type { ClientInput, ClientOutput } from '@honorest/client'

// Get input type for an endpoint
type GetUserInput = ClientInput<typeof UsersContract.endpoints.getUser>
// { params: { id: string } }

// Get output type for an endpoint
type GetUserOutput = ClientOutput<typeof UsersContract.endpoints.getUser>
// { id: string, name: string, email: string }
```

## Best Practices

### 1. Create a Shared API Instance

```typescript
// api.ts
import { createClient } from '@honorest/client'
import { AppContract } from '@myapp/api-contract'

export const api = createClient(AppContract, {
  baseUrl: process.env.API_URL || 'http://localhost:3000',
  headers: {
    'Content-Type': 'application/json'
  }
})
```

### 2. Handle Errors Consistently

```typescript
async function fetchUser(id: string) {
  try {
    return await api.users.getUser({ params: { id }})
  } catch (error) {
    if (error instanceof ClientError) {
      if (error.is(404)) {
        return null // User not found
      }
      throw new Error(`Failed to fetch user: ${error.message}`)
    }
    throw error
  }
}
```

### 3. Use TypeScript Strict Mode

```json
{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true
  }
}
```

## Troubleshooting

### CORS Errors

If you're getting CORS errors, make sure your server allows requests from your client origin:

```typescript
// Server-side (HonorestJS)
app.use('*', cors({
  origin: 'http://localhost:5173', // Your client URL
  credentials: true
}))
```

### TypeScript Errors

Make sure you have the correct versions:
- `@honorest/contract`: `^0.1.1`
- `typescript`: `^5.8.3`

## Contributing

Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.

## License

MIT © [HonorestJS](https://github.com/honorestjs)