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
Every time a DTO changes or a new route is added, someone has to update the client types. Duck Gen removes that step.
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.
Quick start
Install the package
bun add -d @gentleduck/gen
Create 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-gen
You should see output like:
Config loaded
Processing done
Import 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 barrel
If 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'
@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"
}
}
# Run directly
bunx duck-gen
# Or via script
bun run generate
CLI 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.