Skip to main content

client methods

Detailed reference for every Duck Query client method. Covers get, post, put, patch, del, request, byMethod, and the axios instance.

Overview

createDuckQueryClient<Routes>() returns an object with eight members. Every method is generic over the route map, so TypeScript checks paths, request shapes, and response types at compile time.

import { createDuckQueryClient } from '@gentleduck/query'
import type { ApiRoutes } from '@gentleduck/gen/nestjs'
 
const client = createDuckQueryClient<ApiRoutes>({
  baseURL: 'http://localhost:3000',
})
import { createDuckQueryClient } from '@gentleduck/query'
import type { ApiRoutes } from '@gentleduck/gen/nestjs'
 
const client = createDuckQueryClient<ApiRoutes>({
  baseURL: 'http://localhost:3000',
})

createDuckQueryClient

Factory that builds a typed client.

function createDuckQueryClient<Routes>(
  options?: AxiosInstance | AxiosRequestConfig,
): DuckQueryClient<Routes>
function createDuckQueryClient<Routes>(
  options?: AxiosInstance | AxiosRequestConfig,
): DuckQueryClient<Routes>

Parameters:

ParameterTypeDescription
optionsAxiosInstance or AxiosRequestConfig or undefinedAn existing Axios instance, or a config object for a new one.

Examples:

// With config (most common)
const client = createDuckQueryClient<ApiRoutes>({
  baseURL: 'http://localhost:3000',
  withCredentials: true,
  timeout: 10000,
})
 
// With existing Axios instance
import axios from 'axios'
 
const axiosInstance = axios.create({
  baseURL: 'http://localhost:3000',
  headers: { 'X-App': 'my-app' },
})
 
const client = createDuckQueryClient<ApiRoutes>(axiosInstance)
 
// With no config (useful when baseURL is set via interceptor)
const client = createDuckQueryClient<ApiRoutes>()
// With config (most common)
const client = createDuckQueryClient<ApiRoutes>({
  baseURL: 'http://localhost:3000',
  withCredentials: true,
  timeout: 10000,
})
 
// With existing Axios instance
import axios from 'axios'
 
const axiosInstance = axios.create({
  baseURL: 'http://localhost:3000',
  headers: { 'X-App': 'my-app' },
})
 
const client = createDuckQueryClient<ApiRoutes>(axiosInstance)
 
// With no config (useful when baseURL is set via interceptor)
const client = createDuckQueryClient<ApiRoutes>()

get

Sends a GET request. The body is dropped, even if req has one.

client.get<P extends PathsByMethod<Routes, 'GET'>>(
  path: P,
  req?: RouteReqMethod<Routes, P, 'GET'>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteResMethod<Routes, P, 'GET'>>>
client.get<P extends PathsByMethod<Routes, 'GET'>>(
  path: P,
  req?: RouteReqMethod<Routes, P, 'GET'>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteResMethod<Routes, P, 'GET'>>>

TypeScript constraint: the path must be a GET-compatible route. client.get('/api/auth/signin') is a type error if the route only accepts POST.

Examples:

// Simple GET
const { data: users } = await client.get('/api/users')
 
// GET with query parameters
const { data: users } = await client.get('/api/users', {
  query: { page: 1, limit: 20, sort: 'name' },
})
 
// GET with path params
const { data: user } = await client.get('/api/users/:id', {
  params: { id: 'u_123' },
})
 
// GET with path params + query + headers
const { data: user } = await client.get('/api/users/:id', {
  params: { id: 'u_123' },
  query: { include: 'profile' },
  headers: { authorization: 'Bearer tok_abc' },
})
 
// GET with extra Axios config
const { data, headers } = await client.get('/api/users', undefined, {
  responseType: 'json',
  timeout: 5000,
})
// Simple GET
const { data: users } = await client.get('/api/users')
 
// GET with query parameters
const { data: users } = await client.get('/api/users', {
  query: { page: 1, limit: 20, sort: 'name' },
})
 
// GET with path params
const { data: user } = await client.get('/api/users/:id', {
  params: { id: 'u_123' },
})
 
// GET with path params + query + headers
const { data: user } = await client.get('/api/users/:id', {
  params: { id: 'u_123' },
  query: { include: 'profile' },
  headers: { authorization: 'Bearer tok_abc' },
})
 
// GET with extra Axios config
const { data, headers } = await client.get('/api/users', undefined, {
  responseType: 'json',
  timeout: 5000,
})

post

Sends a POST request with a JSON body.

client.post<P extends PathsByMethod<Routes, 'POST'>>(
  path: P,
  req: RouteReqMethod<Routes, P, 'POST'>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteResMethod<Routes, P, 'POST'>>>
client.post<P extends PathsByMethod<Routes, 'POST'>>(
  path: P,
  req: RouteReqMethod<Routes, P, 'POST'>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteResMethod<Routes, P, 'POST'>>>

Note: req is required for POST (unlike GET).

Examples:

// POST with body
const { data: session } = await client.post('/api/auth/signin', {
  body: {
    username: 'duck',
    password: '123456',
  },
})
 
// POST with body + extra config
const { data } = await client.post(
  '/api/auth/signin',
  {
    body: { username: 'duck', password: '123456' },
  },
  {
    withCredentials: true,
    timeout: 10000,
  },
)
 
// POST with body + query params
const { data } = await client.post('/api/files/upload', {
  body: { name: 'report.pdf', size: 1024 },
  query: { overwrite: true },
})
// POST with body
const { data: session } = await client.post('/api/auth/signin', {
  body: {
    username: 'duck',
    password: '123456',
  },
})
 
// POST with body + extra config
const { data } = await client.post(
  '/api/auth/signin',
  {
    body: { username: 'duck', password: '123456' },
  },
  {
    withCredentials: true,
    timeout: 10000,
  },
)
 
// POST with body + query params
const { data } = await client.post('/api/files/upload', {
  body: { name: 'report.pdf', size: 1024 },
  query: { overwrite: true },
})

put

Sends a PUT request with a JSON body — use for full resource updates.

client.put<P extends PathsByMethod<Routes, 'PUT'>>(
  path: P,
  req: RouteReqMethod<Routes, P, 'PUT'>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteResMethod<Routes, P, 'PUT'>>>
client.put<P extends PathsByMethod<Routes, 'PUT'>>(
  path: P,
  req: RouteReqMethod<Routes, P, 'PUT'>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteResMethod<Routes, P, 'PUT'>>>

Examples:

const { data: updated } = await client.put('/api/users/:id', {
  params: { id: 'u_123' },
  body: {
    name: 'Updated Duck',
    email: 'duck@example.com',
  },
})
const { data: updated } = await client.put('/api/users/:id', {
  params: { id: 'u_123' },
  body: {
    name: 'Updated Duck',
    email: 'duck@example.com',
  },
})

patch

Sends a PATCH request with a JSON body — use for partial updates.

client.patch<P extends PathsByMethod<Routes, 'PATCH'>>(
  path: P,
  req: RouteReqMethod<Routes, P, 'PATCH'>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteResMethod<Routes, P, 'PATCH'>>>
client.patch<P extends PathsByMethod<Routes, 'PATCH'>>(
  path: P,
  req: RouteReqMethod<Routes, P, 'PATCH'>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteResMethod<Routes, P, 'PATCH'>>>

Examples:

const { data: updated } = await client.patch('/api/users/:id', {
  params: { id: 'u_123' },
  body: {
    name: 'Patched Duck',  // only update the name
  },
})
const { data: updated } = await client.patch('/api/users/:id', {
  params: { id: 'u_123' },
  body: {
    name: 'Patched Duck',  // only update the name
  },
})

del

Sends a DELETE request. The body is dropped.

client.del<P extends PathsByMethod<Routes, 'DELETE'>>(
  path: P,
  req?: RouteReqMethod<Routes, P, 'DELETE'>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteResMethod<Routes, P, 'DELETE'>>>
client.del<P extends PathsByMethod<Routes, 'DELETE'>>(
  path: P,
  req?: RouteReqMethod<Routes, P, 'DELETE'>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteResMethod<Routes, P, 'DELETE'>>>

Note: named del because delete is a reserved word in JavaScript.

Examples:

// Simple delete
await client.del('/api/users/:id', {
  params: { id: 'u_123' },
})
 
// Delete with headers
await client.del('/api/users/:id', {
  params: { id: 'u_123' },
  headers: { authorization: 'Bearer tok_abc' },
})
// Simple delete
await client.del('/api/users/:id', {
  params: { id: 'u_123' },
})
 
// Delete with headers
await client.del('/api/users/:id', {
  params: { id: 'u_123' },
  headers: { authorization: 'Bearer tok_abc' },
})

request

Generic method that reads config.method for the HTTP verb. Defaults to GET.

client.request<P extends RoutePath<Routes>>(
  path: P,
  req?: RouteReq<Routes, P>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteRes<Routes, P>>>
client.request<P extends RoutePath<Routes>>(
  path: P,
  req?: RouteReq<Routes, P>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteRes<Routes, P>>>

Examples:

// Defaults to GET
const { data } = await client.request('/api/users')
 
// Explicit method via config
const { data } = await client.request(
  '/api/auth/signin',
  { body: { username: 'duck', password: '123456' } },
  { method: 'POST' },
)
// Defaults to GET
const { data } = await client.request('/api/users')
 
// Explicit method via config
const { data } = await client.request(
  '/api/auth/signin',
  { body: { username: 'duck', password: '123456' } },
  { method: 'POST' },
)

Use request when the method is dynamic. For static calls, prefer get, post, and friends for tighter types.

byMethod

Pass the HTTP method as the first argument — this gives the strongest type checking. The path is constrained to routes supporting that method.

client.byMethod<M extends RouteMethods<Routes>, P extends PathsByMethod<Routes, M>>(
  method: M,
  path: P,
  req?: RouteReqMethod<Routes, P, M>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteResMethod<Routes, P, M>>>
client.byMethod<M extends RouteMethods<Routes>, P extends PathsByMethod<Routes, M>>(
  method: M,
  path: P,
  req?: RouteReqMethod<Routes, P, M>,
  config?: AxiosRequestConfig,
): Promise<AxiosResponse<RouteResMethod<Routes, P, M>>>

Examples:

const { data } = await client.byMethod('POST', '/api/auth/signin', {
  body: { username: 'duck', password: '123456' },
})
 
const { data } = await client.byMethod('GET', '/api/users/:id', {
  params: { id: 'u_123' },
})
const { data } = await client.byMethod('POST', '/api/auth/signin', {
  body: { username: 'duck', password: '123456' },
})
 
const { data } = await client.byMethod('GET', '/api/users/:id', {
  params: { id: 'u_123' },
})

Use byMethod when the method is a variable:

async function call(method: 'GET' | 'POST', path: string) {
  return client.byMethod(method, path as any)
}
async function call(method: 'GET' | 'POST', path: string) {
  return client.byMethod(method, path as any)
}

axios

The underlying Axios instance. Use it for interceptors, defaults, or untyped requests.

// Add a request interceptor
client.axios.interceptors.request.use((config) => {
  const token = getToken()
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})
 
// Add a response interceptor
client.axios.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      redirectToLogin()
    }
    return Promise.reject(error)
  },
)
 
// Access defaults
client.axios.defaults.timeout = 10000
// Add a request interceptor
client.axios.interceptors.request.use((config) => {
  const token = getToken()
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})
 
// Add a response interceptor
client.axios.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      redirectToLogin()
    }
    return Promise.reject(error)
  },
)
 
// Access defaults
client.axios.defaults.timeout = 10000

Config merging behavior

When req and config both set the same field, req wins:

FieldComes fromFallback
params (query string)req.queryconfig.params
headersreq.headersconfig.headers
data (body)req.bodyNot applicable
// req.query wins over config.params
await client.get('/api/users', {
  query: { page: 2 },       // this is used
}, {
  params: { page: 1 },      // this is ignored
})
// req.query wins over config.params
await client.get('/api/users', {
  query: { page: 2 },       // this is used
}, {
  params: { page: 1 },      // this is ignored
})

Next steps