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.
What is Duck Gen?
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 or hoping your client stays in sync with the server, Duck Gen
automates the entire contract layer.
It is part of the @gentleduck ecosystem and is currently tested with NestJS. The architecture supports multiple frameworks. NestJS is the first adapter shipped.
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 AuthSessionEvery time a DTO changes or a new route is added, someone has to update the client types. Duck Gen removes that step entirely.
Change your server code, re-run duck-gen, and your client types update automatically.
If the new types break your client code, TypeScript tells you immediately, not your users
in production.
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, just types.
How it works
Loading diagram...
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) to parse your code with full type resolution. This means it understands generics, utility types, type aliases, and complex return types, not just simple interfaces.
Quick start
Install the package
bun 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"
}
}
}The $schema field gives you autocomplete and validation in your editor.
Run the generator
bunx duck-genYou should see output like:
Config 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'>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 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'Use the package entrypoint (@gentleduck/gen/nestjs) for most cases. It works in both
monorepo and standalone setups. Use custom output paths only when you need types in a
package that doesn't have access to @gentleduck/gen.
CLI usage
Add a script to your package.json for convenience:
{
"scripts": {
"generate": "duck-gen",
"generate:watch": "duck-gen --watch"
}
}# 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. - Safe for CI/CD pipelines. Deterministic output, no side effects.
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. Issues, fixes, and documentation
improvements are welcome at @gentleduck.