duck gen
Type-safe API route and message key generator for TypeScript. Scans your server code and emits .d.ts files so your client types always match your backend contracts.
@gentleduck/gen is deprecated and no longer maintained. Use the
NestJS-native toolchain instead — it covers the same surface (typed routes,
typed bodies, runtime validation, SDK generation) with a real compiler
transformer, full tooling, and an active community.
- nestia — typed
@TypedRoute/@TypedBody/@TypedQuery/@TypedParamdecorators, automatic SDK generation, Swagger - typia — runtime validators, serializers, JSON schema generation from TypeScript types via a TS transformer plugin
Migration: replace @Body(typiaBody<X>()) with @TypedBody(), @Get() with
@TypedRoute.Get(). See the nestia migration guide.
What is Duck Gen?
Duck Gen is a compiler extension that reads your server source code and generates TypeScript
definition files (.d.ts) for every API route and message key it finds. It removes the step of
writing route types by hand to match the server.
Duck Gen ships with a NestJS adapter. The architecture supports multiple frameworks; NestJS is the first one.
The problem it solves
Without Duck Gen, keeping client types aligned with server routes looks like this:
// 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 AuthSessionEvery time a DTO changes or a new route is added, someone has to update the client types. Duck Gen removes that step.
Change the server, re-run duck-gen, and the client types update. If the new types
break client code, TypeScript surfaces the error at build time instead of at runtime.
What it generates
Duck Gen produces two categories of types:
| Category | What you get |
|---|---|
| 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.
How it works
Here is the step-by-step flow:
-
Load config: Duck Gen reads
duck-gen.jsonfrom your project root. This tells it which framework adapter to use, where yourtsconfig.jsonlives, and what to generate. -
Build the project: Using ts-morph, it creates an in-memory TypeScript project from your
tsconfigPath. Theincludeandexcludeglobs in yourtsconfig.jsondetermine which files get scanned. -
Scan source files: The NestJS adapter looks for:
- Classes decorated with
@Controller(), these become API routes. - Exported variables with
@duckgenJSDoc tags, these become message sources.
- Classes decorated with
-
Extract type information: For each controller method, Duck Gen extracts:
- The HTTP method (
GET,POST, etc.) from decorators. - The full route path (
globalPrefix+ controller path + method path). - Request shape from parameter decorators (
@Body,@Query,@Param,@Headers). - Response type from the method's return type.
- The HTTP method (
-
Emit
.d.tsfiles: Generated type files are written to the package'sgeneratedfolder and optionally to custom output directories.
Duck Gen uses ts-morph (a TypeScript compiler wrapper) with full type resolution. It understands generics, utility types, type aliases, and nested return types, not just plain interfaces.
Quick start
Install the package
bun add -d @gentleduck/genbun add -d @gentleduck/genCreate duck-gen.json in your project root
{
"$schema": "node_modules/@gentleduck/gen/duck-gen.schema.json",
"framework": "nestjs",
"extensions": {
"shared": {
"includeNodeModules": false,
"outputSource": "./generated",
"sourceGlobs": ["src/**/*.ts", "src/**/*.tsx"],
"tsconfigPath": "./tsconfig.json"
},
"apiRoutes": {
"enabled": true,
"globalPrefix": "/api",
"normalizeAnyToUnknown": true,
"outputSource": "./generated"
},
"messages": {
"enabled": true,
"outputSource": "./generated"
}
}
}{
"$schema": "node_modules/@gentleduck/gen/duck-gen.schema.json",
"framework": "nestjs",
"extensions": {
"shared": {
"includeNodeModules": false,
"outputSource": "./generated",
"sourceGlobs": ["src/**/*.ts", "src/**/*.tsx"],
"tsconfigPath": "./tsconfig.json"
},
"apiRoutes": {
"enabled": true,
"globalPrefix": "/api",
"normalizeAnyToUnknown": true,
"outputSource": "./generated"
},
"messages": {
"enabled": true,
"outputSource": "./generated"
}
}
}The $schema field gives you autocomplete and validation in your editor.
Run the generator
bunx duck-genbunx duck-genYou should see output like:
Config loaded
Processing doneConfig loaded
Processing doneImport and use the generated types
import type { ApiRoutes, RouteReq, RouteRes } from '@gentleduck/gen/nestjs'
// Now your client knows every route, request shape, and response type
type SigninRequest = RouteReq<'/api/auth/signin'>
type SigninResponse = RouteRes<'/api/auth/signin'>import type { ApiRoutes, RouteReq, RouteRes } from '@gentleduck/gen/nestjs'
// Now your client knows every route, request shape, and response type
type SigninRequest = RouteReq<'/api/auth/signin'>
type SigninResponse = RouteRes<'/api/auth/signin'>Output files
Duck Gen always writes to the package's generated folder:
node_modules/@gentleduck/gen/
generated/
nestjs/
duck-gen-api-routes.d.ts # route types
duck-gen-messages.d.ts # message types
index.d.ts # barrel export
index.d.ts # top-level barrelnode_modules/@gentleduck/gen/
generated/
nestjs/
duck-gen-api-routes.d.ts # route types
duck-gen-messages.d.ts # message types
index.d.ts # barrel export
index.d.ts # top-level barrelIf you set outputSource in your config, Duck Gen also copies the generated files to those
locations (e.g. ./generated in your project root).
Import paths:
// From the package entrypoint (recommended)
import type { ApiRoutes, RouteReq, RouteRes } from '@gentleduck/gen/nestjs'
// From a custom output directory
import type { ApiRoutes } from './generated/duck-gen-api-routes'// From the package entrypoint (recommended)
import type { ApiRoutes, RouteReq, RouteRes } from '@gentleduck/gen/nestjs'
// From a custom output directory
import type { ApiRoutes } from './generated/duck-gen-api-routes'Use the package entrypoint (@gentleduck/gen/nestjs) by default. It works in monorepo
and standalone setups. Use a custom output path only when you need types in a package
that cannot import @gentleduck/gen.
CLI usage
Add a script to your package.json for convenience:
{
"scripts": {
"generate": "duck-gen",
"generate:watch": "duck-gen --watch"
}
}{
"scripts": {
"generate": "duck-gen",
"generate:watch": "duck-gen --watch"
}
}# Run directly
bunx duck-gen
# Or via script
bun run generate# Run directly
bunx duck-gen
# Or via script
bun run generateCLI behavior:
- Requires
duck-gen.jsonin the current directory. Fails if missing. - Overwrites existing generated files on every run. Safe to run repeatedly.
- Prints warnings for
anyreturn types and duplicate message const names. - Deterministic output. Safe for CI/CD pipelines.
What to read next
| Guide | What you will learn |
|---|---|
| Configuration | Every config option explained with examples. |
| API Routes | How route scanning works, supported decorators, request shape rules, and examples. |
| Messages | How message scanning works, tag formats, i18n type generation. |
| Generated Types | Deep dive into every exported type with usage examples. |
| Duck Query | Use generated types with a type-safe HTTP client. |
| Templates | Complete NestJS + Duck Query example project. |
Troubleshooting
| Problem | Solution |
|---|---|
| No routes generated | Make sure decorators use string literal paths. Dynamic values are skipped. |
| Missing types in output | Check that tsconfigPath points to the right file and your tsconfig.json includes the source files. |
Message keys are just string | Add as const to your message arrays or objects. |
| Duplicate group key warnings | Each @duckgen group key must be unique across your project. |
any return warnings | Add explicit return types to controller methods, or set normalizeAnyToUnknown: false. |
| Config file not found | Run duck-gen from the directory containing duck-gen.json. |
Requirements
- Bun 1.3.5 or newer.
- A TypeScript project with a valid
tsconfig.json. - NestJS (for the current tested adapter).
Contributing
Duck Gen lives in the Gentleduck monorepo. Open issues and pull requests at
@gentleduck.