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.
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 AuthSessionDuck 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
| Category | Output |
|---|---|
| API route types | A route map with typed request shapes (body, query, params, headers) and response types for every controller method. |
| Message registry types | Strongly-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/genbunx @gentleduck/genDuck 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 checkingconst res = await axios.post('/api/auth/signin', {
emial: 'test@example.com', // typo -- no error
password: '123',
})
// res.data is `any` -- no type checkingAfter (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 typedconst 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 typedThe 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 -> UserProfileimport { 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 -> UserProfileGetting Started
Duck Gen Docs
Configuration, generated types, API routes, and message registries.
Duck Query Docs
Client setup, typed methods, advanced usage, and custom route maps.
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/queryDuck Gen is framework-aware. NestJS is the first supported framework, with more adapters planned.
Duck Gen produces .d.ts files only -- pure types that disappear at compile time. Duck Query is a thin wrapper over Axios that adds type inference without adding bundle weight.