## What is Duck Query?

Duck Query is a **type-safe HTTP client** built on [Axios](https://axios-http.com/). It
takes a route map (generated by Duck Gen or written by hand) and returns a client where
every request path, body, query parameter, and response is checked at compile time.

Write `client.post()` and TypeScript tells you what the route expects and what it returns.

## How it works with Duck Gen

## Install

Install Duck Query

```bash
bun add @gentleduck/query
```

Axios is a **peer dependency**, so it should already be in your project. If not:

```bash
bun add axios
```

Optional: install Duck Gen for generated types

```bash
bun add -d @gentleduck/gen
```

You can also use Duck Query without Duck Gen by defining your own route types.
See [Using without Duck Gen](/duck-query/advanced#using-without-duck-gen).

## Quick start

### With Duck Gen types

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

// Create a typed client
const client = createDuckQueryClient<ApiRoutes>({
  baseURL: 'http://localhost:3000',
  withCredentials: true,
})

// Every call is fully typed
const { data } = await client.post('/api/auth/signin', {
  body: {
    username: 'duck',    // TypeScript enforces this matches SigninDto
    password: '123456',
  },
})
// data is typed as AuthSession (or whatever your controller returns)
```

### With custom route types

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

type MyRoutes = {
  '/ping': {
    method: 'GET'
    params: never
    query: never
    headers: never
    body: never
    res: { ok: true; timestamp: number }
  }
  '/users/:id': {
    method: 'GET'
    params: { id: string }
    query: { include?: 'profile' | 'settings' }
    headers: { authorization: string }
    body: never
    res: { id: string; name: string }
  }
}

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

const { data } = await client.get('/ping')
// data: { ok: true; timestamp: number }
```

## Request shape

Every client method accepts a **request object** with up to four fields:

| Field | Description | Used by |
| --- | --- | --- |
| `body` | JSON request body | POST, PUT, PATCH |
| `query` | Query string parameters | All methods |
| `params` | Path parameters (replaces `:id` placeholders) | All methods |
| `headers` | Per-request headers | All methods |

```ts
await client.get('/api/users/:id', {
  params: { id: 'u_123' },           // => URL becomes /api/users/u_123
  query: { include: 'profile' },     // => ?include=profile
  headers: { authorization: 'Bearer tok_abc' },
})
```

**Only the fields that apply to a route are required.** If a route has no body (like GET),
the `body` field is omitted from the type. Duck Gen's `CleanupNever` utility strips `never`
fields automatically.

### How params work

Path parameters replace `:paramName` placeholders in the URL:

```ts
// Route: '/api/users/:userId/posts/:postId'
await client.get('/api/users/:userId/posts/:postId', {
  params: { userId: 'u_123', postId: 'p_456' },
})
// Actual URL: /api/users/u_123/posts/p_456
```

Values are automatically URL-encoded.

## Client methods

Duck Query provides seven methods plus access to the underlying Axios instance:

| Method | Description | Body sent? |
| --- | --- | --- |
| `get(path, req?, config?)` | GET request | No |
| `post(path, req, config?)` | POST request | Yes |
| `put(path, req, config?)` | PUT request | Yes |
| `patch(path, req, config?)` | PATCH request | Yes |
| `del(path, req?, config?)` | DELETE request | No |
| `request(path, req?, config?)` | Uses `config.method` (default: GET) | Depends |
| `byMethod(method, path, req?, config?)` | Explicit method string | Depends |
| `axios` | The underlying Axios instance | - |

Each method returns `Promise<AxiosResponse<RouteRes>>`, so you get the full Axios response
including `data`, `status`, `headers`, etc.

See [Client Methods](/duck-query/client-methods) for detailed documentation of each method.

## Error handling

Duck Query returns Axios responses, so errors are standard Axios errors:

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

try {
  const { data } = await client.post('/api/auth/signin', {
    body: { username: 'duck', password: 'wrong' },
  })
  return data
} catch (error) {
  if (isAxiosError(error)) {
    console.error('Status:', error.response?.status)
    console.error('Data:', error.response?.data)
  }
  throw error
}
```

## What to read next

| Guide | What you will learn |
| --- | --- |
| [Client Methods](/duck-query/client-methods) | Detailed docs for every client method with examples. |
| [Types](/duck-query/types) | All exported types and how to use them for custom route maps. |
| [Advanced](/duck-query/advanced) | Interceptors, custom Axios instances, using without Duck Gen, and patterns. |
| [Templates](/duck-query/advanced) | Complete NestJS + Duck Query example project. |