type-safe apis with duck gen
How Duck Gen eliminates the gap between your server routes and client code. A deep dive into the compiler extension workflow.
The Type Gap
Server and client end up speaking different type languages.
You define a DTO on the server, write a matching type on the client, and someone renames a field. The client keeps compiling. The bug ships.
Duck Gen closes the gap.
The Workflow
Step 1: Write your server code normally
// src/auth/auth.controller.ts
@Controller('auth')
export class AuthController {
@Post('signin')
signin(@Body() body: SigninDto): Promise<AuthSession> {
return this.authService.signin(body)
}
@Post('signup')
signup(@Body() body: SignupDto): Promise<AuthSession> {
return this.authService.signup(body)
}
}
Step 2: Generate types
bunx @gentleduck/gen
Duck Gen parses your sources with ts-morph, finds every @Controller, and extracts:
- HTTP method decorators (
@Get,@Post,@Put,@Delete,@Patch) - Route paths (controller prefix + method path)
- Parameter decorators (
@Body,@Query,@Param,@Headers) - Return types
Step 3: Use typed routes on the client
import { createDuckQuery } from '@gentleduck/query'
import type { ApiRoutes } from './generated/api-routes'
const client = createDuckQuery<ApiRoutes>({
baseURL: 'https://api.example.com',
})
// Fully typed — body shape, response type, route path
const session = await client.post('/api/auth/signin', {
body: { email: 'user@example.com', password: 'secret' },
})
// session.data is AuthSession
What Gets Generated
The output is a single .d.ts file with a complete route map:
// generated/api-routes.d.ts — 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 }
}
}
A pure type file — no runtime code, no bundle impact. It disappears at compile time.
Configuration
{
"framework": "nestjs",
"srcDir": "./src",
"outputDir": "./generated",
"apiPrefix": "/api",
"include": ["**/*.controller.ts"],
"exclude": ["**/*.spec.ts"]
}
Save this as duck-gen.json in your project root. Duck Gen reads it automatically.
Why Not tRPC / GraphQL Codegen?
| Duck Gen | tRPC | GraphQL Codegen | |
|---|---|---|---|
| Server framework | NestJS (REST) | Custom router | GraphQL servers |
| Client coupling | None — types only | Tight coupling | Schema-first |
| Runtime cost | Zero (.d.ts only) | Minimal | Varies |
| Migration effort | Drop-in — no server changes | Full rewrite | Schema migration |
| Existing REST APIs | Works immediately | Not applicable | Not applicable |
Duck Gen is for teams with existing REST APIs that want type safety without a server rewrite.
Zero server changes
Duck Gen reads your existing NestJS controllers. No new decorators, no architectural changes, no new router. The server code stays the same.
Getting Started
bun add -d @gentleduck/gen # Dev dependency — runs at build time
bun add @gentleduck/query # Runtime — typed HTTP client