Skip to main content
Search...

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/query

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

bun add axios

Optional: install Duck Gen for generated types

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.

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:

FieldDescriptionUsed by
bodyJSON request bodyPOST, PUT, PATCH
queryQuery string parametersAll methods
paramsPath parameters (replaces :id placeholders)All methods
headersPer-request headersAll 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_456

Values are automatically URL-encoded.

Client methods

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

MethodDescriptionBody sent?
get(path, req?, config?)GET requestNo
post(path, req, config?)POST requestYes
put(path, req, config?)PUT requestYes
patch(path, req, config?)PATCH requestYes
del(path, req?, config?)DELETE requestNo
request(path, req?, config?)Uses config.method (default: GET)Depends
byMethod(method, path, req?, config?)Explicit method stringDepends
axiosThe 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
}
GuideWhat you will learn
Client MethodsDetailed docs for every client method with examples.
TypesAll exported types and how to use them for custom route maps.
AdvancedInterceptors, custom Axios instances, using without Duck Gen, and patterns.
TemplatesComplete NestJS + Duck Query example project.