duck gen and duck query

Type-safe API generation and HTTP client for TypeScript. Scan your server code, emit .d.ts files, and get fully typed requests and responses with zero runtime cost.

Duck Gen documentation

The Problem

Every team that builds a TypeScript client and server hits the same wall: keeping types in sync.

You add a route on the server. You update the DTO. Then you go to the client and manually write matching types. Maybe you forget. Maybe the types drift. Maybe someone changes a field name and the client silently breaks.

// Server: you add a new route
@Post('signup')
signup(@Body() body: SignupDto): Promise<AuthSession> { ... }
 
// Client: you manually write the matching types... or forget to
type SignupReq = { email: string; password: string }  // hope this matches SignupDto
type SignupRes = { token: string }                     // hope this matches AuthSession
// Server: you add a new route
@Post('signup')
signup(@Body() body: SignupDto): Promise<AuthSession> { ... }
 
// Client: you manually write the matching types... or forget to
type SignupReq = { email: string; password: string }  // hope this matches SignupDto
type SignupRes = { token: string }                     // hope this matches AuthSession

Duck Gen and Duck Query exist to eliminate this entire category of bugs.


Duck Gen -- The Compiler Extension

Duck Gen is a compiler extension that reads your server source code and generates TypeScript definition files (.d.ts) describing every API route and message key it finds.

Instead of writing route types by hand, Duck Gen automates the entire contract layer.

What it generates

CategoryOutput
API route typesA route map with typed request shapes (body, query, params, headers) and response types for every controller method.
Message registry typesStrongly-typed i18n dictionaries derived from @duckgen message tags in your code.

Both outputs are .d.ts files you import directly -- no runtime cost, just types.

How it works

Loading diagram...

Duck Gen uses ts-morph to parse your source files statically. It finds every @Controller class, extracts the HTTP method decorators (@Get, @Post, @Put, @Delete, @Patch), resolves the full route path, and reads the parameter decorators (@Body, @Query, @Param, @Headers) to determine request shapes. Response types come from the method's return type.

The result is a complete route map that looks like this:

// Generated by duck-gen -- do not edit
export interface ApiRoutes {
  '/api/auth/signin': {
    POST: {
      body: SigninDto
      res: AuthSession
    }
  }
  '/api/auth/signup': {
    POST: {
      body: SignupDto
      res: AuthSession
    }
  }
  '/api/users/:id': {
    GET: {
      params: { id: string }
      res: UserProfile
    }
    PATCH: {
      params: { id: string }
      body: UpdateUserDto
      res: UserProfile
    }
  }
}
// Generated by duck-gen -- do not edit
export interface ApiRoutes {
  '/api/auth/signin': {
    POST: {
      body: SigninDto
      res: AuthSession
    }
  }
  '/api/auth/signup': {
    POST: {
      body: SignupDto
      res: AuthSession
    }
  }
  '/api/users/:id': {
    GET: {
      params: { id: string }
      res: UserProfile
    }
    PATCH: {
      params: { id: string }
      body: UpdateUserDto
      res: UserProfile
    }
  }
}

Configuration

Duck Gen reads a duck-gen.json file in your project root:

{
  "framework": "nestjs",
  "srcDir": "./src",
  "outputDir": "./generated",
  "apiPrefix": "/api",
  "include": ["**/*.controller.ts"],
  "exclude": ["**/*.spec.ts"]
}
{
  "framework": "nestjs",
  "srcDir": "./src",
  "outputDir": "./generated",
  "apiPrefix": "/api",
  "include": ["**/*.controller.ts"],
  "exclude": ["**/*.spec.ts"]
}

One command generates everything:

bunx @gentleduck/gen
bunx @gentleduck/gen

Duck Query -- The Type-Safe Client

Duck Query is a type-safe HTTP client built on Axios. It takes the route map generated by Duck Gen and gives you a client where every request path, body, query parameter, and response is type-checked at compile time.

Before and after

Before (manual types, no safety):

const res = await axios.post('/api/auth/signin', {
  emial: 'test@example.com',  // typo -- no error
  password: '123',
})
// res.data is `any` -- no type checking
const res = await axios.post('/api/auth/signin', {
  emial: 'test@example.com',  // typo -- no error
  password: '123',
})
// res.data is `any` -- no type checking

After (Duck Query + Duck Gen):

const res = await client.post('/api/auth/signin', {
  emial: 'test@example.com',  // TypeScript error: 'emial' does not exist on SigninDto
  password: '123',
})
// res.data is AuthSession -- fully typed
const res = await client.post('/api/auth/signin', {
  emial: 'test@example.com',  // TypeScript error: 'emial' does not exist on SigninDto
  password: '123',
})
// res.data is AuthSession -- fully typed

The full pipeline

Loading diagram...

Setup

import { createDuckQuery } from '@gentleduck/query'
import type { ApiRoutes } from './generated/api-routes'
 
const client = createDuckQuery<ApiRoutes>({
  baseURL: 'https://api.example.com',
})
 
// Every method is fully typed
const session = await client.post('/api/auth/signin', {
  body: { email: 'user@example.com', password: 'secret' },
})
// session.data -> AuthSession
 
const user = await client.get('/api/users/:id', {
  params: { id: '123' },
})
// user.data -> UserProfile
import { createDuckQuery } from '@gentleduck/query'
import type { ApiRoutes } from './generated/api-routes'
 
const client = createDuckQuery<ApiRoutes>({
  baseURL: 'https://api.example.com',
})
 
// Every method is fully typed
const session = await client.post('/api/auth/signin', {
  body: { email: 'user@example.com', password: 'secret' },
})
// session.data -> AuthSession
 
const user = await client.get('/api/users/:id', {
  params: { id: '123' },
})
// user.data -> UserProfile

Getting Started

Install

# Install Duck Gen (dev dependency -- only runs at build time)
bun add -d @gentleduck/gen
 
# Install Duck Query (runtime dependency)
bun add @gentleduck/query
# Install Duck Gen (dev dependency -- only runs at build time)
bun add -d @gentleduck/gen
 
# Install Duck Query (runtime dependency)
bun add @gentleduck/query

Duck Gen is framework-aware. NestJS is the first supported framework, with more adapters planned.