duck query
Type-safe Axios HTTP client that uses route metadata from Duck Gen (or custom route maps) to give you fully typed requests and responses.
What is Duck Query?
Duck Query is a type-safe HTTP client built on Axios. It takes a route map (generated by Duck Gen or written by hand) and gives you a client where every request path, body, query parameter, and response is type-checked at compile time.
No more guessing what a route expects. No more any-typed responses. Write client.post()
and TypeScript tells you exactly what to send and what you get back.
How it works with Duck Gen
Loading diagram...
Install
Install Duck Query
bun add @gentleduck/queryAxios is a peer dependency, so it should already be in your project. If not:
bun add axiosOptional: install Duck Gen for generated types
bun add -d @gentleduck/genYou can also use Duck Query without Duck Gen by defining your own route types. See Using without Duck Gen.
Quick start
With Duck Gen types
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
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 |
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:
// 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_456Values 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 for detailed documentation of each method.
Error handling
Duck Query returns Axios responses, so errors are standard Axios errors:
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 | Detailed docs for every client method with examples. |
| Types | All exported types and how to use them for custom route maps. |
| Advanced | Interceptors, custom Axios instances, using without Duck Gen, and patterns. |
| Templates | Complete NestJS + Duck Query example project. |