## Interceptors

Duck Query is built on Axios, so request and response interceptors are available
through `client.axios`.

### Auth token interceptor

Attach an auth token to every request:

```ts
import { createDuckQueryClient } from '@gentleduck/query'
import type { ApiRoutes } from '@gentleduck/gen/nestjs'

const client = createDuckQueryClient<ApiRoutes>({
  baseURL: 'http://localhost:3000',
})

client.axios.interceptors.request.use((config) => {
  const token = localStorage.getItem('auth_token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})
```

### Token refresh interceptor

Refresh expired tokens and retry the request:

```ts
client.axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true

      const newToken = await refreshToken()
      localStorage.setItem('auth_token', newToken)
      originalRequest.headers.Authorization = `Bearer ${newToken}`

      return client.axios(originalRequest)
    }

    return Promise.reject(error)
  },
)
```

### Logging interceptor

Log requests and responses for debugging:

```ts
client.axios.interceptors.request.use((config) => {
  console.log(`>> ${config.method?.toUpperCase()} ${config.url}`)
  return config
})

client.axios.interceptors.response.use(
  (response) => {
    console.log(`<< ${response.status} ${response.config.url}`)
    return response
  },
  (error) => {
    console.error(`!! ${error.response?.status} ${error.config?.url}`)
    return Promise.reject(error)
  },
)
```

## Error handling patterns

### Using isAxiosError

```ts
import { isAxiosError } from 'axios'

async function signin(username: string, password: string) {
  try {
    const { data } = await client.post('/api/auth/signin', {
      body: { username, password },
    })
    return { ok: true, data } as const
  } catch (error) {
    if (isAxiosError(error)) {
      return {
        ok: false,
        status: error.response?.status ?? 500,
        message: error.response?.data?.message ?? 'Unknown error',
      } as const
    }
    throw error // re-throw non-Axios errors
  }
}

const result = await signin('duck', '123456')
if (result.ok) {
  console.log('Signed in:', result.data)
} else {
  console.error(`Error ${result.status}: ${result.message}`)
}
```

### Typed error responses

If the server uses a consistent error shape, type it:

```ts
type ApiError = {
  state: 'error'
  message: string
  data: null
}

async function safePost<P extends PathsByMethod<ApiRoutes, 'POST'>>(
  path: P,
  req: RouteReqMethod<ApiRoutes, P, 'POST'>,
) {
  try {
    const { data } = await client.post(path, req)
    return { ok: true, data } as const
  } catch (error) {
    if (isAxiosError<ApiError>(error) && error.response) {
      return { ok: false, error: error.response.data } as const
    }
    throw error
  }
}
```

## Using a custom Axios instance

Pass an existing Axios instance instead of a config object. Useful for sharing one
instance across clients or for setups that need special configuration.

```ts
import axios from 'axios'
import { createDuckQueryClient } from '@gentleduck/query'
import type { ApiRoutes } from '@gentleduck/gen/nestjs'

// Create and configure an Axios instance
const axiosInstance = axios.create({
  baseURL: 'http://localhost:3000',
  timeout: 15000,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json',
    'X-App-Version': '1.0.0',
  },
})

// Add interceptors to the instance
axiosInstance.interceptors.request.use((config) => {
  config.headers.Authorization = `Bearer ${getToken()}`
  return config
})

// Pass the instance to Duck Query
const client = createDuckQueryClient<ApiRoutes>(axiosInstance)

// The client uses your configured instance
const { data } = await client.get('/api/users')
```

## Using without Duck Gen

Duck Query takes any route map that follows the `DuckRouteMeta` shape. Duck Gen is not
required.

### Defining routes manually

```ts
import { createDuckQueryClient, type DuckRouteMeta } from '@gentleduck/query'

// Define your routes
type Routes = {
  '/ping': {
    method: 'GET'
    params: never
    query: never
    headers: never
    body: never
    res: { ok: true }
  }
  '/echo': {
    method: 'POST'
    params: never
    query: never
    headers: never
    body: { message: string }
    res: { echo: string }
  }
}

const client = createDuckQueryClient<Routes>({
  baseURL: 'http://localhost:3000',
})

// Type-safe without any code generation
const { data } = await client.get('/ping')
// data: { ok: true }
```

### Sharing route types between server and client

In a monorepo, put route types in a shared package:

```ts title="packages/shared/routes.ts"
export type Routes = {
  '/api/users': {
    method: 'GET'
    params: never
    query: { page?: number; limit?: number }
    headers: never
    body: never
    res: { users: User[]; total: number }
  }
  '/api/users/:id': {
    method: 'GET'
    params: { id: string }
    query: never
    headers: never
    body: never
    res: User
  }
}

export type User = {
  id: string
  name: string
  email: string
}
```

```ts title="apps/client/api.ts"
import { createDuckQueryClient } from '@gentleduck/query'
import type { Routes } from '@monorepo/shared/routes'

export const api = createDuckQueryClient<Routes>({
  baseURL: process.env.API_URL,
})
```

## React Query integration

Duck Query pairs well with [TanStack Query](https://tanstack.com/query) (React Query):

```ts title="hooks/useUser.ts"
import { useQuery, useMutation } from '@tanstack/react-query'
import { createDuckQueryClient } from '@gentleduck/query'
import type { ApiRoutes } from '@gentleduck/gen/nestjs'

const client = createDuckQueryClient<ApiRoutes>({
  baseURL: 'http://localhost:3000',
})

export function useUser(id: string) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: async () => {
      const { data } = await client.get('/api/users/:id', {
        params: { id },
      })
      return data
    },
  })
}

export function useSignin() {
  return useMutation({
    mutationFn: async (body: { username: string; password: string }) => {
      const { data } = await client.post('/api/auth/signin', { body })
      return data
    },
  })
}
```

```tsx title="components/UserProfile.tsx"
function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading, error } = useUser(userId)

  if (isLoading) return <p>Loading...</p>
  if (error) return <p>Error loading user</p>

  return <h1>{user.name}</h1> // user is fully typed
}
```

## Multiple clients

Create separate clients per API or environment:

```ts
import { createDuckQueryClient } from '@gentleduck/query'
import type { ApiRoutes } from '@gentleduck/gen/nestjs'

// Main API client
export const api = createDuckQueryClient<ApiRoutes>({
  baseURL: 'http://localhost:3000',
  withCredentials: true,
})

// Admin API client with different auth
export const adminApi = createDuckQueryClient<ApiRoutes>({
  baseURL: 'http://localhost:3000',
  headers: { 'X-Admin-Key': process.env.ADMIN_KEY },
})

// External service client with custom route map
type ExternalRoutes = {
  '/status': { method: 'GET'; params: never; query: never; headers: never; body: never; res: { up: boolean } }
}

export const external = createDuckQueryClient<ExternalRoutes>({
  baseURL: 'https://external-service.com',
})
```

## Behavior details

* **GET and DELETE drop the body**: a `body` field on the request is ignored for GET,
  DELETE, HEAD, and OPTIONS.
* **`request` defaults to GET**: no `config.method` means `'GET'`.
* **Path type checking**: `client.post()` only accepts POST-capable paths. Posting to a
  GET-only route is a compile-time error.
* **Param encoding**: path params run through `encodeURIComponent`.
* **Slash normalization**: double slashes in built URLs collapse to one.

## Next steps

* [Duck Query overview](/duck-query): getting started guide.
* [Client Methods](/duck-query/client-methods): detailed method reference.
* [Types](/duck-query/types): all exported types.
* [Templates](/duck-query/advanced): complete working example.